diff --git a/Editor/IncrementalBuildPipeline/BeeBuildProgramCommon.Data/BeeBuildProgramCommon.Data.gen.csproj b/Editor/IncrementalBuildPipeline/BeeBuildProgramCommon.Data/BeeBuildProgramCommon.Data.gen.csproj
index ef251769b5..06c46083d7 100644
--- a/Editor/IncrementalBuildPipeline/BeeBuildProgramCommon.Data/BeeBuildProgramCommon.Data.gen.csproj
+++ b/Editor/IncrementalBuildPipeline/BeeBuildProgramCommon.Data/BeeBuildProgramCommon.Data.gen.csproj
@@ -19,11 +19,15 @@
false
false
+
+ $(HOME)
+ $(USERPROFILE)
+
-
+
diff --git a/Editor/IncrementalBuildPipeline/PlayerBuildProgramLibrary.Data/PlayerBuildProgramLibrary.Data.gen.csproj b/Editor/IncrementalBuildPipeline/PlayerBuildProgramLibrary.Data/PlayerBuildProgramLibrary.Data.gen.csproj
index 1d221ff092..ccd864db2e 100644
--- a/Editor/IncrementalBuildPipeline/PlayerBuildProgramLibrary.Data/PlayerBuildProgramLibrary.Data.gen.csproj
+++ b/Editor/IncrementalBuildPipeline/PlayerBuildProgramLibrary.Data/PlayerBuildProgramLibrary.Data.gen.csproj
@@ -20,12 +20,16 @@
false
PlayerBuildProgramLibrary.Data
+
+ $(HOME)
+ $(USERPROFILE)
+
-
+
diff --git a/Editor/IncrementalBuildPipeline/ScriptCompilationBuildProgram.Data/ScriptCompilationBuildProgram.Data.gen.csproj b/Editor/IncrementalBuildPipeline/ScriptCompilationBuildProgram.Data/ScriptCompilationBuildProgram.Data.gen.csproj
index 19b9d56861..7c75a60369 100644
--- a/Editor/IncrementalBuildPipeline/ScriptCompilationBuildProgram.Data/ScriptCompilationBuildProgram.Data.gen.csproj
+++ b/Editor/IncrementalBuildPipeline/ScriptCompilationBuildProgram.Data/ScriptCompilationBuildProgram.Data.gen.csproj
@@ -19,12 +19,16 @@
false
false
+
+ $(HOME)
+ $(USERPROFILE)
+
-
+
diff --git a/Editor/Mono/2D/SpriteAtlas/EditorSpriteAtlas.bindings.cs b/Editor/Mono/2D/SpriteAtlas/EditorSpriteAtlas.bindings.cs
index febe5c2d84..95d2a717df 100644
--- a/Editor/Mono/2D/SpriteAtlas/EditorSpriteAtlas.bindings.cs
+++ b/Editor/Mono/2D/SpriteAtlas/EditorSpriteAtlas.bindings.cs
@@ -37,6 +37,9 @@ public static void PackAtlases(SpriteAtlas[] atlases, BuildTarget target, bool c
[FreeFunction("SpriteAtlasExtensions::CleanupAtlasPacking")]
extern public static void CleanupAtlasPacking();
+
+ [FreeFunction("SpriteAtlasExtensions::OnSpriteAtlasSettingsChanged")]
+ extern internal static void OnSpriteAtlasSettingsChanged();
}
diff --git a/Editor/Mono/2D/SpriteAtlas/SpriteAtlasImporterInspector.cs b/Editor/Mono/2D/SpriteAtlas/SpriteAtlasImporterInspector.cs
index 9fde0c7d30..2d61e1d87a 100644
--- a/Editor/Mono/2D/SpriteAtlas/SpriteAtlasImporterInspector.cs
+++ b/Editor/Mono/2D/SpriteAtlas/SpriteAtlasImporterInspector.cs
@@ -317,6 +317,8 @@ private SerializedObject GetSerializedAssetObject()
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_FilterMode = serializedObject.FindProperty("m_TextureSettings.filterMode");
m_AnisoLevel = serializedObject.FindProperty("m_TextureSettings.anisoLevel");
diff --git a/Editor/Mono/Animation/AnimationEventWrapperInspector.cs b/Editor/Mono/Animation/AnimationEventWrapperInspector.cs
index ed34a5f4f1..c7195072e8 100644
--- a/Editor/Mono/Animation/AnimationEventWrapperInspector.cs
+++ b/Editor/Mono/Animation/AnimationEventWrapperInspector.cs
@@ -101,16 +101,21 @@ public static void OnEditAnimationEvents(AnimationEventWrapper[] awEvents, Anima
EditorGUIUtility.labelWidth = 130;
EditorGUI.showMixedValue = !singleFunctionName;
- int wasSelected = singleFunctionName ? selected : -1;
+
+ EditorGUI.BeginChangeCheck();
selected = EditorGUILayout.Popup("Function: ", selected, methodsFormatted);
- if (wasSelected != selected && selected != -1 && selected != notSupportedIndex)
+ if (EditorGUI.EndChangeCheck())
{
- foreach (var evt in data.selectedEvents)
+ if (selected >= 0 && selected < notSupportedIndex)
{
- evt.functionName = supportedMethods[selected].Name;
- evt.stringParameter = string.Empty;
+ foreach (var evt in data.selectedEvents)
+ {
+ evt.functionName = supportedMethods[selected].Name;
+ evt.stringParameter = string.Empty;
+ }
}
}
+
EditorGUI.showMixedValue = false;
EditorGUI.indentLevel++;
diff --git a/Editor/Mono/Animation/CurveEditor/CurveEditorSelection.cs b/Editor/Mono/Animation/CurveEditor/CurveEditorSelection.cs
index f7fa130907..da072f4806 100644
--- a/Editor/Mono/Animation/CurveEditor/CurveEditorSelection.cs
+++ b/Editor/Mono/Animation/CurveEditor/CurveEditorSelection.cs
@@ -51,15 +51,15 @@ internal CurveSelection(int curveID, int key, SelectionType type)
public int CompareTo(CurveSelection other)
{
- int cmp = curveID - other.curveID;
+ int cmp = curveID.CompareTo(other.curveID);
if (cmp != 0)
return cmp;
- cmp = key - other.key;
+ cmp = key.CompareTo(other.key);
if (cmp != 0)
return cmp;
- return (int)type - (int)other.type;
+ return ((int)type).CompareTo((int)other.type);
}
public override bool Equals(object _other)
diff --git a/Editor/Mono/Animation/EditorCurveBinding.bindings.cs b/Editor/Mono/Animation/EditorCurveBinding.bindings.cs
index cab5880b58..c822207a91 100644
--- a/Editor/Mono/Animation/EditorCurveBinding.bindings.cs
+++ b/Editor/Mono/Animation/EditorCurveBinding.bindings.cs
@@ -74,7 +74,7 @@ public struct EditorCurveBinding : IEquatable
public override int GetHashCode()
{
- return String.Format("{0}:{1}:{2}", path, type.Name, propertyName).GetHashCode();
+ return HashCode.Combine(path, type, propertyName);
}
public override bool Equals(object other)
diff --git a/Editor/Mono/AssemblyInfo/AssemblyInfo.cs b/Editor/Mono/AssemblyInfo/AssemblyInfo.cs
index 45fd161ca0..89a060d2ed 100644
--- a/Editor/Mono/AssemblyInfo/AssemblyInfo.cs
+++ b/Editor/Mono/AssemblyInfo/AssemblyInfo.cs
@@ -221,6 +221,7 @@
[assembly:InternalsVisibleTo("Unity.AI.Navigation.Editor")]
[assembly: InternalsVisibleTo("Unity.Scenes")]
+[assembly: InternalsVisibleTo("Unity.Scenes.Editor.Tests")]
[assembly: InternalsVisibleTo("UnityEditor.Switch.Tests")]
@@ -252,6 +253,7 @@
[assembly: InternalsVisibleTo("Unity.Modules.BuildProfileEditor.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.UI.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.Multiplayer.Server.Tests.Editor")]
+[assembly: InternalsVisibleTo("Unity.Modules.Multiplayer.PlayMode.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.AssetDatabase.AssetPostProcessor.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.AssetPackage.Tests.Editor")]
// This should move with the AnimationWindow to a module at some point
@@ -259,6 +261,9 @@
[assembly: InternalsVisibleTo("Unity.Modules.Physics.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.GI.EditorBake.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.GI.LightProbes.Tests.Editor")]
+[assembly: InternalsVisibleTo("Unity.Modules.GI.UVUnwrap.Tests.Editor")]
+[assembly: InternalsVisibleTo("Unity.Modules.GI.BakedGI.ColorSpace.Tests.Editor")]
+[assembly: InternalsVisibleTo("Unity.Modules.GI.LightingInspectorMeshRenderer.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.Physics2D.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.U2D.NineSlice.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.GI.Prefabs.Tests.Editor")]
@@ -266,6 +271,7 @@
[assembly: InternalsVisibleTo("Unity.Modules.ShaderCompilationEditor.ShaderCompilation.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.ShaderCompilationEditor.ShaderUtil.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.ShaderCompilationEditor.Rendering.Tests.Editor")]
+[assembly: InternalsVisibleTo("Unity.Modules.ShaderCompilationEditor.ShaderCacheConsistency.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.Tests.Shared")]
diff --git a/Editor/Mono/AssetDatabase/AssetDatabaseSearching.cs b/Editor/Mono/AssetDatabase/AssetDatabaseSearching.cs
index 4b27ab4cab..5a452d519d 100644
--- a/Editor/Mono/AssetDatabase/AssetDatabaseSearching.cs
+++ b/Editor/Mono/AssetDatabase/AssetDatabaseSearching.cs
@@ -45,6 +45,7 @@ public static GUID[] FindAssetGUIDs(string filter, string[] searchInFolders)
var searchFilter = CreateSearchFilter(filter, searchInFolders);
return FindAssetGUIDs(searchFilter);
}
+ [VisibleToOtherModules("UnityEditor.UIToolkitAuthoringModule")]
internal static string[] FindAssets(SearchFilter searchFilter)
{
#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
diff --git a/Editor/Mono/AssetPipeline/SpeedTree/SpeedTree9ImporterEditor.cs b/Editor/Mono/AssetPipeline/SpeedTree/SpeedTree9ImporterEditor.cs
index a2a468257b..a2d6c5053d 100644
--- a/Editor/Mono/AssetPipeline/SpeedTree/SpeedTree9ImporterEditor.cs
+++ b/Editor/Mono/AssetPipeline/SpeedTree/SpeedTree9ImporterEditor.cs
@@ -46,6 +46,12 @@ internal IEnumerable importers
public override void OnEnable()
{
+ if (!AreImporterTargetsValid())
+ {
+ base.OnEnable(); // lets the base mark the editor enabled/inert (OnDisable symmetry)
+ return;
+ }
+
m_STImporter = target as SpeedTree9Importer;
if (tabs == null)
diff --git a/Editor/Mono/AssetPostprocessor.cs b/Editor/Mono/AssetPostprocessor.cs
index 7428a15723..70ab8a5933 100644
--- a/Editor/Mono/AssetPostprocessor.cs
+++ b/Editor/Mono/AssetPostprocessor.cs
@@ -126,12 +126,17 @@ public class MethodInfoCallback : Callback
public bool MethodDomainReload { get; }
+ // Cached: PostprocessAllAssets runs for every import batch and building this
+ // marker name there would allocate a string per callback per batch.
+ public string PerformanceMarkerName { get; }
+
public override string name => classType.FullName;
public MethodInfoCallback(MethodInfo method, bool methodDomainReload)
{
Method = method;
MethodDomainReload = methodDomainReload;
+ PerformanceMarkerName = $"{method.DeclaringType.Name}.OnPostprocessAllAssets";
}
public override IEnumerable GetCustomAttributes() => Method.GetCustomAttributes();
@@ -323,7 +328,7 @@ static void PostprocessAllAssets(string[] importedAssets, string[] addedAssets,
{
if (assetPostProcessor.MethodDomainReload)
{
- using (new EditorPerformanceMarker($"{assetPostProcessor.classType.Name}.OnPostprocessAllAssets", assetPostProcessor.classType).Auto())
+ using (new EditorPerformanceMarker(assetPostProcessor.PerformanceMarkerName, assetPostProcessor.classType).Auto())
InvokeMethod(assetPostProcessor.Method, argsWithDidDomainReload);
}
else
@@ -331,7 +336,7 @@ static void PostprocessAllAssets(string[] importedAssets, string[] addedAssets,
if (containsNoAssets)
continue;
- using (new EditorPerformanceMarker($"{assetPostProcessor.classType.Name}.OnPostprocessAllAssets", assetPostProcessor.classType).Auto())
+ using (new EditorPerformanceMarker(assetPostProcessor.PerformanceMarkerName, assetPostProcessor.classType).Auto())
InvokeMethod(assetPostProcessor.Method, args);
}
}
diff --git a/Editor/Mono/BuildPipeline/AssemblyStripper.cs b/Editor/Mono/BuildPipeline/AssemblyStripper.cs
index 946b0e4da7..ff2c4a5b48 100644
--- a/Editor/Mono/BuildPipeline/AssemblyStripper.cs
+++ b/Editor/Mono/BuildPipeline/AssemblyStripper.cs
@@ -266,13 +266,15 @@ public static string WriteEditorData(BuildPostProcessArgs args, NPath linkerInpu
{
CollectIncludedAndExcludedModules(out var forceIncludeModules, out var forceExcludeModules);
- var editorToLinkerData = new EditorToLinkerData
- {
#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- typesInScenes = GetTypesInScenesInformation(args.report, args.usedClassRegistry)
+ var typesInScenes = GetTypesInScenesInformation(args.report, args.usedClassRegistry)
+ .OrderBy(data => data.fullManagedTypeName ?? data.nativeClass)
+ .ToArray();
#pragma warning restore UA2001
- .OrderBy(data => data.fullManagedTypeName ?? data.nativeClass)
- .ToArray(),
+
+ var editorToLinkerData = new EditorToLinkerData
+ {
+ typesInScenes = typesInScenes,
allNativeTypes = CollectNativeTypeData().ToArray(),
forceIncludeModules = forceIncludeModules.ToArray(),
forceExcludeModules = forceExcludeModules.ToArray()
@@ -291,9 +293,10 @@ public static string WriteEditorData(BuildPostProcessArgs args, NPath linkerInpu
var unityType = UnityType.FindTypeByName(nativeClass);
var managedName = RuntimeClassMetadataUtils.ScriptingWrapperTypeNameForNativeID(unityType.persistentTypeID);
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- var usedInScenes = rcr.GetScenesForClass(unityType.persistentTypeID)?.OrderBy(p => p);
-#pragma warning restore UA2001
+
+ var usedInScenes = rcr.GetScenesForClass(unityType.persistentTypeID)?.ToArray();
+ if (usedInScenes != null)
+ Array.Sort(usedInScenes);
bool noManagedType = unityType.persistentTypeID != 0 && managedName == "UnityEngine.Object";
var information = new EditorToLinkerData.TypeInSceneData(
@@ -301,9 +304,7 @@ public static string WriteEditorData(BuildPostProcessArgs args, NPath linkerInpu
noManagedType ? null : managedName,
nativeClass,
unityType.module,
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- usedInScenes != null ? usedInScenes.ToArray() : null);
-#pragma warning restore UA2001
+ usedInScenes);
items.Add(information);
}
@@ -407,26 +408,14 @@ private static string GetMethodPreserveBlacklistContents(RuntimeClassRegistry rc
#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
var groupedByAssembly = rcr.GetMethodsToPreserve().GroupBy(m => m.assembly);
-#pragma warning restore UA2001
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
foreach (var assembly in groupedByAssembly.OrderBy(a => a.Key))
-#pragma warning restore UA2001
{
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
var assemblyName = assembly.Key;
-#pragma warning restore UA2001
sb.AppendLine(string.Format("\t", assemblyName));
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
var groupedByType = assembly.GroupBy(m => m.fullTypeName);
-#pragma warning restore UA2001
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
foreach (var type in groupedByType.OrderBy(t => t.Key))
-#pragma warning restore UA2001
{
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
sb.AppendLine(string.Format("\t\t", EscapeXmlString(type.Key)));
-#pragma warning restore UA2001
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
foreach (var method in type.OrderBy(m => m.methodName))
#pragma warning restore UA2001
sb.AppendLine(string.Format("\t\t\t", EscapeXmlString(method.methodName)));
diff --git a/Editor/Mono/BuildPipeline/BuildPipeline.bindings.cs b/Editor/Mono/BuildPipeline/BuildPipeline.bindings.cs
index 80a4f0e31d..135a4706c6 100644
--- a/Editor/Mono/BuildPipeline/BuildPipeline.bindings.cs
+++ b/Editor/Mono/BuildPipeline/BuildPipeline.bindings.cs
@@ -425,13 +425,19 @@ internal static BuildReport BuildPlayerData(BuildPlayerDataOptions buildPlayerDa
/// Builds a content directory (serialized assets and scenes plus a manifest) at a defined output path.
///
///
- /// Register the folder at runtime with .
- /// Each entry in must be a ScriptableObject ; the build includes those roots and
- /// everything they reference (including , , and ).
- /// Creates an outputPath if missing, normalizes path separators, and defaults to the output folder name.
+ /// Use buildParameters to configure the build, including the root assets to include, the output
+ /// path, and optional compression and build options. Each entry in
+ /// must be a . The build includes those root assets and everything they reference, such as
+ /// assets referenced through , , and
+ /// .
+ ///
+ /// The method creates if it doesn't exist, normalizes path
+ /// separators, and defaults to the output folder name.
///
/// The build uses and the active subtarget configured for that target in the build settings.
/// Select the intended platform in the **Build Profile** window or through [command line arguments](xref:um-command-line-arguments) so that the active target is set to the desired setting prior to calling this method.
+ ///
+ /// To load the built content, register the output directory with .
///
///
///
@@ -525,6 +531,7 @@ public static BuildReport BuildContentDirectory(BuildContentDirectoryParameters
///
///
///
+ ///
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)
{
BuildAssetBundlesParameters input = new BuildAssetBundlesParameters
@@ -730,6 +737,7 @@ public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBund
///]]>
///
///AssetBundles
+ ///
public static AssetBundleManifest BuildAssetBundles(BuildAssetBundlesParameters buildParameters)
{
if (buildParameters.targetPlatform == 0 || buildParameters.targetPlatform == BuildTarget.NoTarget)
diff --git a/Editor/Mono/BuildPipeline/BuildPipelineInterfaces.cs b/Editor/Mono/BuildPipeline/BuildPipelineInterfaces.cs
index ad3d9db3d1..5091f1b7e7 100644
--- a/Editor/Mono/BuildPipeline/BuildPipelineInterfaces.cs
+++ b/Editor/Mono/BuildPipeline/BuildPipelineInterfaces.cs
@@ -22,7 +22,9 @@ namespace UnityEditor.Build
///Interface that provides control over callback order.
///This is the base class for build callback interfaces, for example , , , , and .
///
- ///Every class that implements these interfaces must define the callbackOrder property with a "get" accessor.
+ ///Every class that implements these interfaces must define the callbackOrder property with a "get" accessor.
+ ///
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
public interface IOrderedCallback
{
///Returns a numeric value that determines the order in which the build callback is invoked.
@@ -85,7 +87,15 @@ public abstract class BuildPlayerProcessor : IOrderedCallback
///Returns the relative callback order for callbacks. Callbacks with lower values are called before ones with higher values.
public virtual int callbackOrder => 0;
///Implement this function to receive a callback before a Player build starts.
- ///You can use this function to customize the build before Unity starts building the Player. For example, the following code example demonstrates how to include streaming assets in the Player build without placing them in your project's StreamingAssets folder.
+ ///You can use this function to customize the build before Unity starts building the Player. For example, the following code example demonstrates how to include streaming assets in the Player build without placing them in your project's StreamingAssets folder.
+ ///
+ ///This callback is a good place to confirm that the project is configured correctly before the build starts, and to fail the build early if a required setting is missing. Give validation callbacks a low value so they run before other PrepareForBuild callbacks.
+ ///
+ ///To fail the build from this callback, throw a . It reports a clear message without a call stack.
+ ///
+ ///Unlike , you can trigger a content-only build from this callback, for example by calling . The Addressables package uses this callback to build content during a Player build.
+ ///
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks).
/// The context for the scheduled Player build.
///
///
///
///
+ ///
public abstract void PrepareForBuild(BuildPlayerContext buildPlayerContext);
}
///Implement this interface to execute code at the start of the Player build process.
- ///This interface is replaced by , which works for AssetBundle and ContentDirectory builds as well.
- ///
- ///At the start of a Player build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
- ///
- ///This callback can be useful for automated tasks and ensuring your build environment is correctly configured.
+ ///This interface behaves like , but is invoked only for Player builds. Use instead, because it is also invoked for AssetBundle and content directory builds.
///
- ///Example usages include:
- ///
- ///* For validation checks, e.g. confirming required build settings, environmental variables, content or other project-specific conditions. When possible you can automatically fix problems by changing settings. Or you can fail the build, by throwing a BuildFailedException along with a clear error message.
- ///* To make sure required Assets are included in the build. See .
- ///* To generate version numbers, change logs, link.xml files or other content that should be regenerated prior to each Player build.
- ///* For logging, reporting or sending analytics.
- ///
- ///Note: Build callbacks are a powerful feature, but it is strongly recommended that their implementations maintain deterministic build outputs.
- ///The result of a build should be predictable and reproducible, based on the project’s content, the Unity version, and installed packages.
- ///Introducing environment-specific behavior, external dependencies, randomness, or other non-deterministic elements can lead to outcomes
- ///that are challenging to debug or reproduce. This unpredictability may also compromise the efficiency and accuracy of incremental builds or incremental upgrades.
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
+ ///
///
///
///
@@ -132,34 +130,35 @@ public abstract class BuildPlayerProcessor : IOrderedCallback
public interface IPreprocessBuildWithReport : IOrderedCallback
{
///Implement this method to receive a callback before the build is started.
- ///This method is replaced by , which works for AssetBundle builds as well.
- /// This callback is invoked during Player builds, but not during AssetBundle builds.
+ ///This method is invoked during Player builds only. Use instead, which is also invoked during AssetBundle and content directory builds.
/// A report containing information about the build, such as its target platform and output path.
///
///
///
void OnPreprocessBuild(BuildReport report);
}
- ///Implement this interface to execute code at the start of the Player build or AssetBundle build process.
- ///At the start of a Player build or AssetBundle build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
+ ///Implement this interface to execute code at the start of the Player, AssetBundle, or content directory build process.
+ ///At the start of a Player, AssetBundle, or content directory build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
///
///This callback can be useful for automated tasks and ensuring your build environment is correctly configured.
///
- ///You can't invoke an additional build from inside this callback. To invoke an AssetBundle build at the start of a Player build you should use instead.
+ ///You can't invoke another build from inside this callback. To invoke a content build at the start of a Player build, use instead.
///
///Example usages include:
///
- ///* For validation checks, e.g. confirming required build settings, environmental variables, content or other project-specific conditions. When possible you can automatically fix problems by changing settings. Or you can fail the build, by throwing a BuildFailedException along with a clear error message.
- ///* To make sure required Assets are included in the build. See .
- ///* To generate version numbers, change logs, link.xml files or other content that should be regenerated prior to each Player build or AssetBundle build.
- ///* For logging, reporting or sending analytics.
+ ///* For validation checks, for example confirming required build settings, environment variables, content, or other project-specific conditions. When possible you can automatically fix problems by changing settings, or you can fail the build.
+ ///* To make sure required Assets are included in the build. Refer to .
+ ///* To generate version numbers, change logs, link.xml files, or other content that must be regenerated before each build.
+ ///* For logging, reporting, or sending analytics.
+ ///
+ ///To fail the build from this callback, throw a . It reports a clear message without a call stack and fails the build whether or not the build uses strict mode. A logged error, such as from Debug.LogError , fails the build only when the build uses strict mode ( , , or ).
///
///Note: Build callbacks are a powerful feature, but it is strongly recommended that their implementations maintain deterministic build outputs.
///The result of a build should be predictable and reproducible, based on the project’s content, the Unity version, and installed packages.
///Introducing environment-specific behavior, external dependencies, randomness, or other non-deterministic elements can lead to outcomes
///that are challenging to debug or reproduce. This unpredictability might compromise the efficiency and accuracy of incremental builds or incremental upgrades.
///
- ///The main difference between this interface and or is that this callback gets called on AssetBundle builds and Player builds.
+ ///The main difference between this interface and or is that this callback is invoked on AssetBundle and content directory builds as well as Player builds.
///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
///
///
///
///
+ ///
public interface IPreprocessBuildWithContext : IOrderedCallback
{
///Implement this method to receive a callback before the build is started.
- ///This callback is invoked during Player builds and AssetBundle builds.
+ ///This callback is invoked during Player, AssetBundle, and content directory builds.
/// A context containing information about the build, such as its build report.
///
///
///
///
+ ///
void OnPreprocessBuild(BuildCallbackContext ctx);
}
@@ -263,18 +264,17 @@ public interface IPostprocessBuild : IOrderedCallback
}
///Implement this interface to execute code immediately after the Player build process is completed.
- ///This interface is replaced by , which works for AssetBundle and ContentDirectory builds as well.
- ///This is useful for tasks that need to be performed as the last step of building, such as cleaning up assets, generating analytics or reports, or customizing build outputs.
+ ///This interface behaves like , but is invoked only for Player builds. Use instead, because it is also invoked for AssetBundle and content directory builds.
///
- ///As a final step of a Player build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
+ ///
///
///
public interface IPostprocessBuildWithReport : IOrderedCallback
{
///Implement this function to receive a callback after the build is complete.
- ///This method is replaced by , which works for AssetBundle builds as well.
- /// This callback is invoked during Player builds, but not during AssetBundle builds.
- /// If the build stops early, due to a failure or cancellation, then the callback is not invoked.
+ ///This method is invoked during Player builds only. Use instead, which is also invoked during AssetBundle and content directory builds.
+ /// If the build stops early, due to a failure or cancellation, then this callback is not invoked.
/// A BuildReport containing information about the build, such as the target platform and output path.
///
///
void OnPostprocessBuild(BuildReport report);
}
- ///Implement this interface to execute code immediately after the Player build or AssetBundle build process is completed.
- ///This is useful for tasks that need to run after a build completes, even if the build failed or was cancelled. For example, you might want to clean up assets, generate analytics or reports, or customize build outputs. The postprocess callback runs whether the build succeeds, fails, or is cancelled, as long as the corresponding callback ran. It's only skipped if early validation prevents the build from starting.
+ ///Implement this interface to execute code immediately after the Player, AssetBundle, or content directory build process is completed.
+ ///This is useful for tasks that need to run after a build completes, even if the build failed or was canceled. For example, you might want to clean up assets, generate analytics or reports, or customize build outputs. The postprocess callback runs whether the build succeeds, fails, or is canceled, as long as the corresponding callback ran. It's only skipped if early validation prevents the build from starting.
+ ///
+ ///As a final step of a Player, AssetBundle, or content directory build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
///
- ///As a final step of a Player build or AssetBundle build, Unity uses the property on each implementation to determine the order in which to invoke the callbacks.
+ ///To fail the build from this callback, throw a . It reports a clear message without a call stack and fails the build whether or not the build uses strict mode. A logged error, such as from Debug.LogError , fails the build only when the build uses strict mode ( , , or ).
///
- ///Note: The main difference between this interface and or is that this callback gets called on AssetBundle builds as well as Player builds.
+ ///Note: The main difference between this interface and or is that this callback is invoked on AssetBundle and content directory builds as well as Player builds.
+ ///
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
///
///
///
///
+ ///
public interface IPostprocessBuildWithContext : IOrderedCallback
{
///Implement this function to receive a callback after the build is complete.
- ///This callback is invoked during Player builds or AssetBundle builds.
+ ///This callback is invoked during Player, AssetBundle, and content directory builds.
/// This callback is invoked even when the build stops early due to a failure or cancellation. However it will not be invoked if initial validation checks prevent the build from starting.
/// A context containing information about the build, such as its build report.
///
@@ -396,6 +401,7 @@ public interface IPostprocessBuildWithContext : IOrderedCallback
///
///
///
+ ///
///
void OnPostprocessBuild(BuildCallbackContext ctx);
}
@@ -405,7 +411,7 @@ public interface IPostprocessBuildWithContext : IOrderedCallback
public interface IPostBuildPlayerScriptDLLs : IOrderedCallback
{
///Implement this interface to receive a callback just after the player scripts have been compiled.
- ///You can implement this if you need to read or patch managed Assemblies for players being built. You can get assembly locations from the files property of the report parameter. Note that implementing this callback will cause builds to run slower, as assemblies need to be copied to an intermediate location, and is not recommended for best performance.
+ ///You can implement this if you need to read or patch managed Assemblies for players being built. To retrieve assembly locations, call GetFiles on the BuildReport provided as the report parameter, and read the path property from each returned BuildFile . Note that implementing this callback causes builds to run slower, as assemblies need to be copied to an intermediate location, and is not recommended for best performance.
/// A report containing information about the build, such as its target platform and output path.
///
///
/// Implement this method to receive a callback for each scene during the build.
///
- /// Unity invokes this callback during Player and AssetBundle builds, and also when a scene is reloaded while entering Play mode. Use to determine in which context the callback is called.
+ /// Unity invokes this callback during Player, AssetBundle, and content directory builds, and also when a scene is loaded while in Play mode. Use to determine in which context the callback is called.
///
/// This callback supports editing the provided scene to prepare it for a Player build or entering Play mode, and reading assets. For example, you can add or remove references to project assets in that scene.
///
- /// This callback doesn't support modifying the state of other assets. Use it to modify only the provided scene.
+ /// This callback doesn't support modifying the state of other assets, creating new assets, or deleting assets. Use it to modify only the provided scene.
+ ///
+ /// Don't access the build history from this callback. For content directory builds this callback can run in a separate worker process where is not available.
+ ///
+ /// To fail the build from this callback, throw a . It reports a clear message without a call stack and fails the build whether or not the build uses strict mode. A logged error, such as from Debug.LogError , fails the build only when the build uses strict mode ( , , or ).
///
/// Keep implementations deterministic. Avoid random values, timestamps, or external changing data sources. For more information, refer to [Deterministic builds](xref:um-build-deterministic-builds).
///
+ /// Use and to add explicit dependencies to assets used during your scene modifications.
+ ///
/// Apply to callback types and increment the version whenever the callback logic changes to help Unity invalidate cached scene processing results when needed.
///
/// For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks).
/// The current scene being processed.
/// A report containing information about the current build. When this callback is invoked for scene loading during Play mode, this parameter is null.
- ///
+ ///
///
- ///
- ///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
void OnProcessScene(UnityEngine.SceneManagement.Scene scene, BuildReport report);
}
@@ -526,7 +540,7 @@ public interface IPreprocessShaders : IOrderedCallback
///- Combinations of keywords that are never used.
///- Keywords you only use in your debug builds.
///
- ///Unity invokes the `OnProcessShader` callback in both Player and AssetBundle builds. If there are any shaders already in the cache, then this method isn't invoked for those shaders. To ensure the callback runs for all shaders, perform a [clean build](xref:um-build-clean-build). To run it for a specific shader, modify that shader or one of its dependent assets.
+ ///Unity invokes the `OnProcessShader` callback in Player, AssetBundle, and content directory builds. If there are any shaders already in the cache, then this method isn't invoked for those shaders. To ensure the callback runs for all shaders, perform a [clean build](xref:um-build-clean-build). To run it for a specific shader, modify that shader or one of its dependent assets.
///
///To help you identify keywords and variants to strip, you can [check what shader variants you have in your project](xref:um-shader-how-many-variants). For example if you [declare a keyword](xref:um-sl-multiple-program-variants) called `DEBUG` in your shader code using `#pragma multi_compile _ DEBUG`, the following [Editor script](xref:um-special-folders) finds and strips shader variants that use the keyword.
///
@@ -537,15 +551,16 @@ public interface IPreprocessShaders : IOrderedCallback
///3. Implements the `OnProcessShader` callback function and iterates over the `data` list, which contains every variant in the shader.
///4. Uses `data.shaderKeywordSet.IsEnabled()` to check if each variant uses the keyword.
///5. Uses `data.removeAt()` to strip a shader variant if it contains the keyword and you've disabled **Development build** in **[Build Settings](xref:um-build-settings)**.
- ///
+ ///
///You can also find local keywords. You must create the `ShaderKeyword` instance inside the implementation of `OnProcessShader`, so you can use the callback's `shader` variable in the `ShaderKeyword` constructor.
///
///For example if you declare a local keyword called `RED` in your shader code using `#pragma multi_compile_local _ RED`, the following script finds and strips shader variants that use the keyword.
- ///
+ ///
///If you strip a variant that a Material needs at runtime, Unity chooses an available shader variant that matches as closely as possible.
///
///Find out about other ways you can [strip shader variants](xref:um-shader-variant-stripping).
///
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
///
/// The shader that Unity is about to compile.
/// Details about the specific shader code being compiled.
@@ -586,6 +601,7 @@ public interface IPreprocessShaders : IOrderedCallback
///
///
///
+ ///
///
void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList data);
}
@@ -605,7 +621,9 @@ public interface IPreprocessComputeShaders : IOrderedCallback
///
///Note that this callback only provides details of compute shaders. To see regular shaders that Unity is about to compile into the build, see .
///
- ///This callback is invoked for both Player and AssetBundle builds.
+ ///This callback is invoked for Player, AssetBundle, and content directory builds.
+ ///
+ ///For more information about build callbacks, refer to [Use build callbacks](xref:um-build-callbacks)
/// The compute shader that Unity is about to compile.
/// The name of the kernel that Unity is about to compile.
/// The list of shader variants that Unity is about to compile.
@@ -655,6 +673,7 @@ public interface IPreprocessComputeShaders : IOrderedCallback
///Declaring and using shader keywords in HLSL
///
///
+ ///
void OnProcessComputeShader(ComputeShader shader, string kernelName, IList data);
}
diff --git a/Editor/Mono/BuildPipeline/BuildPlayerContext.cs b/Editor/Mono/BuildPipeline/BuildPlayerContext.cs
index 5478ad3b6a..8fd1ecedc2 100644
--- a/Editor/Mono/BuildPipeline/BuildPlayerContext.cs
+++ b/Editor/Mono/BuildPipeline/BuildPlayerContext.cs
@@ -36,14 +36,18 @@ internal BuildPlayerContext(BuildPlayerOptions buildPlayerOptions)
///
///
/// This is useful if you want the player build
- /// to retrieve type stripping information from content-only builds you do prior to the player build.
+ /// to retrieve type stripping information from builds you do prior to the Player build.
///
/// If this method is called on the same path multiple times, the path is only added once.
///
/// If the path passed into this method is not a valid build report directory, at build time an error will be thrown.
///
- /// For more information on locating the build report directory for a build, refer to .
+ /// For more information on locating the build report directory for a build, refer to .
+ ///
+ /// For more information, refer to [How code stripping affects content](xref:um-managed-code-stripping-content).
/// The path to a build report directory. If the path is invalid, an error will be thrown during the build process.
+ ///
+ ///
public void AddPreviousBuildReportDirectory(string directory)
{
if (!AdditionalBuildReportDirectories.Contains(directory))
diff --git a/Editor/Mono/BuildPipeline/BuildTarget.cs b/Editor/Mono/BuildPipeline/BuildTarget.cs
index 1209562331..3167055ee0 100644
--- a/Editor/Mono/BuildPipeline/BuildTarget.cs
+++ b/Editor/Mono/BuildPipeline/BuildTarget.cs
@@ -25,7 +25,7 @@ public enum BuildTarget
{
///Build a macOS standalone.
///
- ///To specify which architecture to use (Intel, ARM or Universal), please use .
+ ///To specify which architecture to use (Intel, ARM or Universal), refer to OSXStandalone.UserBuildSettings.architecture .
///
StandaloneOSX = 2,
@@ -156,7 +156,7 @@ public enum BuildTarget
///
tvOS = 37,
- ///Build a Nintendo Switch player.
+ ///Build a Nintendo Switch™ player.
///
Switch = 38,
@@ -199,7 +199,8 @@ public enum BuildTarget
///
VisionOS = 47,
- [ExcludeFromDocs]
+ ///Build a Nintendo Switch™ 2 player.
+ ///
Switch2 = 48,
[ExcludeFromDocs]
diff --git a/Editor/Mono/BuildPipeline/BuildTargetDiscovery.bindings.cs b/Editor/Mono/BuildPipeline/BuildTargetDiscovery.bindings.cs
index eacc7731df..ea8f904991 100644
--- a/Editor/Mono/BuildPipeline/BuildTargetDiscovery.bindings.cs
+++ b/Editor/Mono/BuildPipeline/BuildTargetDiscovery.bindings.cs
@@ -90,6 +90,8 @@ public struct DiscoveredTargetInfo
static readonly string k_SDKProviderNotMultiTargetError = L10n.Tr("The SDK platform provider '{0}' with guid '{1}' references a platform that is not marked as a multi-target platform.");
internal static readonly string k_SDKProviderNotDerivedTargetError = L10n.Tr("The SDK platform provider '{0}' with guid '{1}' references a platform that is not marked as a derived platform.");
static readonly string k_CreateIPlatformProviderFailedError = L10n.Tr("Failed to create IPlatformProvider instance for type '{0}'.");
+ static readonly string k_PlatformDeprecatedDefaultWithDisplayName = L10n.Tr("The {0} platform is deprecated.");
+ static readonly string k_DerivedPlatformUsesDeprecatedBase = L10n.Tr("This platform is based on {0}, which is deprecated.");
public static extern bool PlatformHasFlag(BuildTarget platform, TargetAttributes flag);
@@ -321,6 +323,11 @@ internal struct PlatformInfo
// needs to be removed when https://jira.unity3d.com/browse/PLAT-7721 is implemented
public NameAndLink? temporaryLabelAndLinkForIndustrialOnboarding = null;
+ ///
+ /// Custom deprecation copy for UI when is set. May be empty if a generic fallback should be used.
+ ///
+ public string deprecationMessage = string.Empty;
+
public PlatformInfo() {}
public bool HasFlag(PlatformAttributes flag) { return (flags & flag) == flag; }
@@ -345,6 +352,20 @@ internal class PlatformPackageInfo
public string publisher;
public bool hasThumbnail;
+ ///
+ /// Optional catalog flag (e.g. SDK platform JSON, mock platforms). Used as a fallback deprecation
+ /// signal when Package Manager has no for this package.
+ ///
+ public bool deprecated;
+
+ ///
+ /// Optional deprecation tooltip when is true. Used as a fallback when
+ /// Package Manager has no for this package.
+ ///
+ public string deprecationMessage;
+
+ public PlatformPackageInfo() {}
+
public PlatformPackageInfo(string displayName, string qualifiedName, string description, string publisher = "", bool hasThumbnail = false)
{
this.displayName = displayName;
@@ -954,10 +975,49 @@ public static GUID GetGUIDFromBuildTarget(BuildTarget buildTarget)
return EmptyGuid;
}
+ ///
+ /// Get the server platform GUID corresponding to the NamedBuildTarget.Server and BuildTarget.
+ ///
+ /// The NamedBuildTarget to get the server platform GUID for.
+ /// The BuildTarget to get the server platform GUID for.
+ /// The server platform GUID. Derived server platform GUID when the active platform is a derived server platform. Base server platform GUID otherwise.
+ /// True if a server platform GUID was found; otherwise, false.
internal static bool TryGetServerGUIDFromBuildTarget(NamedBuildTarget namedBuildTarget, BuildTarget buildTarget, out GUID result)
{
result = EmptyGuid;
+ if (namedBuildTarget != NamedBuildTarget.Server || !IsStandalonePlatform(buildTarget))
+ return false;
+
+ if (s_BuildTargetToPlatformGUID.TryGetValue(buildTarget, out var guid))
+ {
+ var module = ModuleManager.FindPlatformSupportModule(guid);
+ if (module is IDerivedBuildTargetProvider)
+ {
+ var derivedPlatformGuid = module.PlatformBuildTarget.Guid;
+ var (_, subtarget) = GetBuildTargetAndSubtargetFromGUID(derivedPlatformGuid);
+ if (subtarget == StandaloneBuildSubtarget.Server)
+ {
+ result = derivedPlatformGuid;
+ return true;
+ }
+ }
+ }
+
+ return TryGetBaseServerGUIDFromBuildTarget(namedBuildTarget, buildTarget, out result);
+ }
+
+ ///
+ /// Get the base server platform GUID corresponding to the NamedBuildTarget.Server and BuildTarget.
+ ///
+ /// The NamedBuildTarget to get the base server platform GUID for.
+ /// The BuildTarget to get the base server platform GUID for.
+ /// The base server platform GUID. If the platform is not a derived platform, the same GUID is returned.
+ /// True if a base server platform GUID was found; otherwise, false.
+ internal static bool TryGetBaseServerGUIDFromBuildTarget(NamedBuildTarget namedBuildTarget, BuildTarget buildTarget, out GUID result)
+ {
+ result = EmptyGuid;
+
if (namedBuildTarget == NamedBuildTarget.Server)
{
foreach (var platform in allPlatforms)
@@ -978,7 +1038,7 @@ internal static bool TryGetServerGUIDFromBuildTarget(NamedBuildTarget namedBuild
internal static GUID GetBasePlatformGUIDFromBuildTarget(NamedBuildTarget namedBuildTarget, BuildTarget buildTarget)
{
- if (TryGetServerGUIDFromBuildTarget(namedBuildTarget, buildTarget, out var value))
+ if (TryGetBaseServerGUIDFromBuildTarget(namedBuildTarget, buildTarget, out var value))
return value;
if (s_BuildTargetToPlatformGUID.TryGetValue(buildTarget, out GUID guid))
@@ -1004,8 +1064,8 @@ internal static GUID GetBasePlatformGUID(GUID platformGuid)
{
if (platformInfo.subtarget != StandaloneBuildSubtarget.Server)
return basePlatformGuid;
-
- if (TryGetServerGUIDFromBuildTarget(NamedBuildTarget.Server, platformInfo.buildTarget, out var serverPlatformGuid))
+
+ if (TryGetBaseServerGUIDFromBuildTarget(NamedBuildTarget.Server, platformInfo.buildTarget, out var serverPlatformGuid))
return serverPlatformGuid;
}
@@ -1094,6 +1154,13 @@ static void LoadSDKPlatforms()
if (string.IsNullOrEmpty(displayName))
Debug.LogWarning(string.Format(k_SDKPlatformMissingDisplayNameWarning, sdkPlatformGuid));
+ if (sdkPlatformInfo.isDeprecated)
+ flags |= PlatformAttributes.IsDeprecated;
+
+ var deprecationMessage = sdkPlatformInfo.isDeprecated && !string.IsNullOrEmpty(sdkPlatformInfo.deprecationMessage)
+ ? sdkPlatformInfo.deprecationMessage
+ : string.Empty;
+
PlatformInfo platformInfo = new()
{
supportedPlatformGuids = sdkPlatformInfo.flags.platformType == SDKPlatformType.MultiTarget ?
@@ -1109,6 +1176,7 @@ static void LoadSDKPlatforms()
buildProfilePlatformBannerBgColorHex = sdkPlatformInfo.bannerBackgroundColorHex ?? "#00000000",
internalPackages = sdkPlatformInfo.internalPackages,
partnerPackages = sdkPlatformInfo.partnerPackages,
+ deprecationMessage = deprecationMessage,
flags = flags | PlatformAttributes.IsSDKPlatform | PlatformAttributes.IsVisibleInPlatformBrowserOnly |
PlatformAttributes.IsWindowsBuildTarget | PlatformAttributes.IsWindowsArm64BuildTarget |
PlatformAttributes.IsLinuxBuildTarget | PlatformAttributes.IsMacBuildTarget,
@@ -1123,6 +1191,11 @@ static void LoadSDKPlatforms()
}
var groupIndex = Array.FindIndex(allPlatformGroups, g => g.groupName == targetGroupName);
+ // SDK platform JSON typically stores the English group identifier (e.g. "Web") while
+ // allPlatformGroups use localized titles (L10n.Tr("Web")). Without this fallback the
+ // platform is registered in allPlatforms but never added to a browser group.
+ if (groupIndex < 0)
+ groupIndex = Array.FindIndex(allPlatformGroups, g => g.groupName == L10n.Tr(targetGroupName));
if (groupIndex < 0)
{
Debug.LogWarning(string.Format(k_SDKPlatformUnknownPlatformGroupWarning, sdkPlatformGuid, targetGroupName));
@@ -1689,6 +1762,58 @@ public static string BuildPlatformSubtitle(GUID guid)
return string.Empty;
}
+ ///
+ /// When the platform is marked deprecated, returns true and sets to the
+ /// configured text. The message may be empty if no custom copy was provided; callers should supply a fallback in that case.
+ /// Base platforms are checked directly. Derived platforms also check their immediate base platform.
+ /// Multi-target platforms are deprecated only when marked deprecated on their own platform entry.
+ ///
+ public static bool BuildPlatformTryGetDeprecationMessage(GUID guid, out string deprecationMessage)
+ {
+ deprecationMessage = string.Empty;
+ if (!TryGetPlatformInfo(guid, out PlatformInfo platformInfo))
+ return false;
+
+ if (platformInfo.HasFlag(PlatformAttributes.IsDeprecated))
+ {
+ deprecationMessage = platformInfo.deprecationMessage;
+ return true;
+ }
+
+ if (platformInfo.HasFlag(PlatformAttributes.IsDerivedBuildTarget))
+ {
+ var basePlatformGuid = GetBasePlatformGUID(guid);
+ if (!basePlatformGuid.Empty()
+ && basePlatformGuid != guid
+ && TryGetDirectDeprecationMessage(basePlatformGuid, out var baseDeprecationMessage))
+ {
+ deprecationMessage = FormatDerivedInheritedDeprecationMessage(basePlatformGuid, baseDeprecationMessage);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static bool TryGetDirectDeprecationMessage(GUID guid, out string deprecationMessage)
+ {
+ deprecationMessage = string.Empty;
+ if (!TryGetPlatformInfo(guid, out PlatformInfo platformInfo) || !platformInfo.HasFlag(PlatformAttributes.IsDeprecated))
+ return false;
+
+ deprecationMessage = platformInfo.deprecationMessage;
+ return true;
+ }
+
+ static string FormatDerivedInheritedDeprecationMessage(GUID deprecatedBasePlatformGuid, string baseDeprecationMessage)
+ {
+ var baseDisplayName = BuildPlatformDisplayName(deprecatedBasePlatformGuid);
+ if (string.IsNullOrWhiteSpace(baseDeprecationMessage))
+ baseDeprecationMessage = string.Format(k_PlatformDeprecatedDefaultWithDisplayName, baseDisplayName);
+
+ return $"{baseDeprecationMessage}\n\n{string.Format(k_DerivedPlatformUsesDeprecatedBase, baseDisplayName)}";
+ }
+
public static PlatformGroup[] GetPlatformGroups()
{
return allPlatformGroups;
diff --git a/Editor/Mono/BuildPipeline/BuildTargetGroup.cs b/Editor/Mono/BuildPipeline/BuildTargetGroup.cs
index 21503d74f8..3d1b2e067a 100644
--- a/Editor/Mono/BuildPipeline/BuildTargetGroup.cs
+++ b/Editor/Mono/BuildPipeline/BuildTargetGroup.cs
@@ -116,7 +116,7 @@ public enum BuildTargetGroup
[ExcludeFromDocs]
Facebook = 26,
- ///Nintendo Switch target.
+ ///Nintendo Switch™ target.
Switch = 27,
[Obsolete("Lumin has been removed in 2022.2")]
@@ -153,7 +153,7 @@ public enum BuildTargetGroup
///Apple visionOS target.
VisionOS = 36,
- [ExcludeFromDocs]
+ ///Nintendo Switch™ 2 target.
Switch2 = 37,
[ExcludeFromDocs]
diff --git a/Editor/Mono/BuildPipeline/DataBuildDirtyTracker.cs b/Editor/Mono/BuildPipeline/DataBuildDirtyTracker.cs
index 05c9cf6783..2374469eb1 100644
--- a/Editor/Mono/BuildPipeline/DataBuildDirtyTracker.cs
+++ b/Editor/Mono/BuildPipeline/DataBuildDirtyTracker.cs
@@ -54,6 +54,10 @@ class BuildData
BuildOptions.CompressWithLz4HC;
public string[] assemblyNames;
+
+ // Session GUID of the full build that produced this data cache.
+ // A scripts-only or incremental build that reuses this cache copies it onto its report.
+ public GUID contentSourceBuildSessionGuid;
}
private BuildData buildData;
@@ -207,7 +211,8 @@ static public void WriteBuildData(string buildDataPath, BuildReport report, stri
.Where(m => ModuleMetadata.GetModuleIncludeSettingForModule(m) != ModuleIncludeSetting.ForceExclude)
.ToArray(),
#pragma warning restore UA2001
- assemblyNames = sortedAssemblyNames
+ assemblyNames = sortedAssemblyNames,
+ contentSourceBuildSessionGuid = report.summary.buildSessionGuid
};
buildDataPath.ToNPath().WriteAllText(JsonUtility.ToJson(buildData));
}
@@ -247,5 +252,30 @@ static public void ForceDirty(string buildDataPath)
NPath buildReportPath = buildDataPath;
buildReportPath.DeleteIfExists();
}
+
+ // Called (from native BuildPlayer) when a build reuses the data cache instead of rebuilding it — both the
+ // explicit scripts-only and the automatic (clean CheckDirty) reuse paths converge here. The reused BuildData
+ // carries the session GUID of the full build that produced the cache; record it on this build's report so it
+ // is written into BuildReportSummary and Build Analysis can borrow that exact source build's asset table.
+ [RequiredByNativeCode]
+ static public void SetContentSourceBuild(BuildReport report, string buildDataPath)
+ {
+ try
+ {
+ NPath path = buildDataPath;
+ if (report == null || !path.FileExists())
+ return;
+
+ var buildData = JsonUtility.FromJson(path.ReadAllText());
+ if (buildData == null || buildData.contentSourceBuildSessionGuid.Empty())
+ return;
+
+ report.SetContentSourceBuildSessionGUID(buildData.contentSourceBuildSessionGuid);
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning($"Failed to record source content build metadata for this scripts-only build: {e.Message}");
+ }
+ }
}
}
diff --git a/Editor/Mono/BuildPipeline/NamedBuildTarget.cs b/Editor/Mono/BuildPipeline/NamedBuildTarget.cs
index 3eadef274c..8edd7f926b 100644
--- a/Editor/Mono/BuildPipeline/NamedBuildTarget.cs
+++ b/Editor/Mono/BuildPipeline/NamedBuildTarget.cs
@@ -8,7 +8,8 @@
namespace UnityEditor.Build
{
- ///Build Target by name. This allows to describe and identify build targets that are not fully represented by BuildTargetGroup and BuildTarget.
+ ///Identifies which platform's build settings an API applies to.
+ ///Use to choose the platform output to build. Use to choose which platform's settings to read or change.
public readonly struct NamedBuildTarget : IEquatable, IComparable
{
private static readonly string[] k_ValidNames =
@@ -40,7 +41,7 @@ namespace UnityEditor.Build
"Kepler",
};
- ///Unknown.
+ ///An unknown or unspecified named build target, typically used as a placeholder.
public static readonly NamedBuildTarget Unknown = new NamedBuildTarget("");
///Desktop Standalone.
public static readonly NamedBuildTarget Standalone = new NamedBuildTarget("Standalone");
diff --git a/Editor/Mono/BuildPipeline/Settings/BuildAssetBundleOptions.bindings.cs b/Editor/Mono/BuildPipeline/Settings/BuildAssetBundleOptions.bindings.cs
index f93cc2aa61..24dddb26dd 100644
--- a/Editor/Mono/BuildPipeline/Settings/BuildAssetBundleOptions.bindings.cs
+++ b/Editor/Mono/BuildPipeline/Settings/BuildAssetBundleOptions.bindings.cs
@@ -137,7 +137,21 @@ public enum BuildAssetBundleOptions
// Force the build to fail when any errors are encountered
///Fails the build if any errors are reported during it.
- ///Without this flag, non-fatal errors - such as a failure to compile a shader for a particular platform - will not cause the build to fail, but may result in incorrect behaviour at runtime.
+ ///Without this flag, non-fatal errors, such as a failure to compile a shader for a particular platform, won't cause the build to fail, but might result in incorrect behavior at runtime.
+ ///
+ /// Always set this flag, unless errors are logged from packages or other third-party code that you can't fix, and you need the build to proceed despite them.
+ ///
+ /// When this flag is set, errors logged from these build callbacks also fail the build:
+ /// ,
+ /// ,
+ /// ,
+ /// , and
+ /// .
+ ///
+ /// This flag is the AssetBundle equivalent of .
+ ///
+ ///
+ ///
///
///
///
diff --git a/Editor/Mono/BuildPipeline/Settings/BuildContentDirectoryParameters.bindings.cs b/Editor/Mono/BuildPipeline/Settings/BuildContentDirectoryParameters.bindings.cs
index aa6927314e..b82e63e32e 100644
--- a/Editor/Mono/BuildPipeline/Settings/BuildContentDirectoryParameters.bindings.cs
+++ b/Editor/Mono/BuildPipeline/Settings/BuildContentDirectoryParameters.bindings.cs
@@ -29,15 +29,17 @@ public struct BuildContentDirectoryParameters
public string outputPath { get; set; }
///
- /// Array of paths to the root Assets that should be included in the build.
+ /// Array of paths to the root assets to include in the build.
///
///
- /// This property should contain project-relative paths to existing ScriptableObject-derived Assets. Each specified
- /// Asset will be included in the build, and available for direct load. Any dependency referenced from the Asset will also
- /// be included in the build. Root assets are automatically loaded when a Content Directory is registered, so only
- /// ScriptableObject-derived assets are permitted to prevent accidental misuse (such as attempting to use large assets
- /// like Textures or Meshes as root assets).
+ /// Set this property to project-relative paths of existing -derived assets. The build
+ /// includes each specified asset along with any dependency it references. After you register the content directory, retrieve
+ /// the root assets at runtime with .
+ ///
+ /// Only ScriptableObject-derived assets are permitted as roots, to prevent accidental misuse such as using large assets
+ /// like Textures or Meshes as root assets.
///
+ ///
public string[] rootAssetPaths { get; set; }
///
@@ -48,6 +50,17 @@ public struct BuildContentDirectoryParameters
///
/// The compression settings for the build. Defaults to .
///
+ ///
+ /// With the default , the build writes the content as individual loose files
+ /// without an archive wrapper. Set to wrap the output in archive (.archive)
+ /// files instead. Any other compression setting always produces archive files.
+ ///
+ /// can load a content directory whether
+ /// its content is stored as loose files or wrapped in archive files. You can also create archive files from loose-file
+ /// output as a separate step after the build with
+ /// .
+ ///
+ ///
public BuildCompression compression { get; set; }
// Internal: optional BuildTarget. When unset at default, native code takes both platform and subtarget from current Editor build settings.
@@ -72,9 +85,14 @@ public struct BuildContentDirectoryParameters
/// Optional name for the build.
///
///
- /// This name is stored in the BuildReport and BuildManifest for identification purposes.
+ /// This name is stored in the BuildReport and BuildManifest for identification purposes. It is reported as
+ /// in the build's , and as
+ /// in the lightweight build summary.
/// If not specified, the leaf folder name of is used as the default.
///
+ ///
+ ///
+ ///
public string name { get; set; }
///
diff --git a/Editor/Mono/BuildPipeline/Settings/BuildContentOptions.bindings.cs b/Editor/Mono/BuildPipeline/Settings/BuildContentOptions.bindings.cs
index 0695dba7b8..e185bfbad9 100644
--- a/Editor/Mono/BuildPipeline/Settings/BuildContentOptions.bindings.cs
+++ b/Editor/Mono/BuildPipeline/Settings/BuildContentOptions.bindings.cs
@@ -27,7 +27,7 @@ public enum BuildContentOptions
///
/// When this flag is set, the build system will package content into archive files with the ".archive" extension,
/// which can improve loading performance and reduce file system overhead.
- ///
+ ///
/// This flag is unnecessary if compression is enabled using the field.
///
UseArchive = 1 << 0,
@@ -45,20 +45,32 @@ public enum BuildContentOptions
///
/// Clear all cached build results, resulting in a full rebuild of content.
- ///
+ ///
+ ///
///
///
CleanBuildCache = 1 << 5,
///
- /// Do not allow the build to succeed if any errors are reported during it.
+ /// Fail the build if any errors are logged while it runs.
///
///
- /// Without this flag, non-fatal errors - such as a failure to compile a shader for a particular platform - will not
- /// cause the build to fail, but may result in incorrect behaviour at runtime.
+ /// Without this flag, non-fatal errors, such as a failure to compile a shader for a particular platform, don't
+ /// cause the build to fail, but might result in incorrect behavior at runtime.
+ ///
+ /// Always set this flag, unless errors are logged from packages or other third-party code that you can't fix, and you need the build to proceed despite them.
+ ///
+ /// When this flag is set, errors logged from these build callbacks also fail the build:
+ /// ,
+ /// ,
+ /// ,
+ /// , and
+ /// .
///
- /// This flag is equivalent to .
+ /// This flag is the content directory equivalent of .
///
+ ///
+ ///
FailBuildWhenErrorsLogged = 1 << 9,
///
diff --git a/Editor/Mono/BuildPipeline/Settings/BuildOptions.bindings.cs b/Editor/Mono/BuildPipeline/Settings/BuildOptions.bindings.cs
index 7afcb1a4f4..dc5abcfa8a 100644
--- a/Editor/Mono/BuildPipeline/Settings/BuildOptions.bindings.cs
+++ b/Editor/Mono/BuildPipeline/Settings/BuildOptions.bindings.cs
@@ -44,7 +44,7 @@ public enum BuildOptions
// Do not overwrite player directory, but accept user's modifications.
///Appends to an existing Xcode (iOS) project during the build process.
- ///This preserves any changes made to the existing Xcode project settings. With the IL2CPP scripting backend, this setting also allows incremental builds of the generated C++ code to work in Xcode. Appending to Xcode projects is supported only on macOS and Windows platforms.
+ ///This preserves any changes made to the existing Xcode project settings. With the IL2CPP scripting backend, this setting also allows incremental builds of the generated C++ code to work in Xcode. Appending to Xcode projects is supported only on macOS and Windows platforms. This option applies to iOS builds; for similar behavior on Android, use instead.
///
AcceptExternalModificationsToPlayer = 1 << 5,
@@ -220,7 +220,21 @@ public enum BuildOptions
// Force the build to fail when any errors are encountered
///Prevent the build from succeeding if any errors are reported during the build process.
- ///Without this flag, non-fatal errors, such as shader compilation issues on a particular platform, won't cause the build to fail, but might lead to incorrect behavior at runtime.
+ ///Without this flag, non-fatal errors, such as shader compilation issues on a particular platform, won't cause the build to fail, but might lead to incorrect behavior at runtime.
+ ///
+ /// Set this flag in most builds. Omit it only when errors are logged from packages or other third-party code that you can't fix, and you need the build to proceed despite them.
+ ///
+ /// This flag is set automatically when you use **Build and Run** in the Editor.
+ ///
+ /// When this flag is set, errors logged from these build callbacks also fail the build:
+ /// ,
+ /// ,
+ /// ,
+ /// , and
+ /// .
+ ///
+ ///
+ ///
StrictMode = 1 << 21,
///Build will include Assemblies for testing.
diff --git a/Editor/Mono/BuildPipeline/Settings/BuildPlayerOptions.bindings.cs b/Editor/Mono/BuildPipeline/Settings/BuildPlayerOptions.bindings.cs
index 86d1b44f04..513bae79b0 100644
--- a/Editor/Mono/BuildPipeline/Settings/BuildPlayerOptions.bindings.cs
+++ b/Editor/Mono/BuildPipeline/Settings/BuildPlayerOptions.bindings.cs
@@ -64,6 +64,11 @@ public struct BuildPlayerOptions
[NativeName("platformGroup")]
public BuildTargetGroup targetGroup { get; set; }
///The to build.
+ ///For best results, leave this property unset so that the build uses the target defined in the active . If you do set it, set it to match the active build target.
+ ///
+ ///Building a target that doesn't match the active build target is unreliable. Changing the active build target requires recompiling Editor scripts for the new platform and a domain reload, which can't happen while a build script is running. As a result, build callbacks and platform-dependent code might compile for the wrong platform, and some platforms fail to build at all.
+ ///
+ ///To select the target, set the active build profile first: use the Build Profiles window in the Editor, or the -activeBuildProfile or -buildTarget argument on the [command line](xref:um-build-command-line). For more information, refer to [Create a custom build script](xref:um-build-script-build).
///
[NativeName("platform")]
public BuildTarget target { get; set; }
@@ -86,14 +91,21 @@ public struct BuildPlayerOptions
///
///
public int subtarget { get; set; }
- ///Additional , like whether to run the built player.
+ ///The flags to apply when building the Player.
+ ///Set this property to a bitwise combination of values before you pass to . For example, set options = BuildOptions.Development | BuildOptions.AutoRunPlayer to create a development build and run it automatically after the build completes.
public BuildOptions options { get; set; }
///The additional preprocessor defines you can specify while compiling assemblies for the Player. These defines are appended to the existing Scripting Define Symbols list configured in the Player settings.
public string[] extraScriptingDefines { get; set; }
- ///Use this property to reference the output folders or build report directories from one or more content directory builds.
+ ///Use this property to reference the output folders or build report directories from one or more builds.
///The types used in those builds are included in the information provided to UnityLinker.
- ///This ensures that the player can load all the content from those additional builds.
+ ///This ensures that the Player can load all the content from those additional builds.
+ ///
+ ///During a build you can also add directories from a build callback with .
+ ///
+ ///For more information, refer to [How code stripping affects content](xref:um-managed-code-stripping-content).
+ ///
+ ///
public string[] previousBuildReportDirectories { get; set; }
[NativeHeader("Editor/Src/BuildPipeline/BuildPlayerOptions.h")]
diff --git a/Editor/Mono/BuildProfile/BuildProfileContext.cs b/Editor/Mono/BuildProfile/BuildProfileContext.cs
index 9934c6af83..f298a810e0 100644
--- a/Editor/Mono/BuildProfile/BuildProfileContext.cs
+++ b/Editor/Mono/BuildProfile/BuildProfileContext.cs
@@ -572,19 +572,17 @@ void OnEnable()
EditorGraphicsSettings.activeProfileHasGraphicsSettings = ActiveProfileHasGraphicsSettings();
- var buildProfile = activeProfile;
+ if (activeProfile != null)
+ return;
- if (buildProfile == null)
- {
- buildProfile = GetForClassicPlatform(EditorUserBuildSettings.activePlatformGuid);
+ var buildProfile = GetForClassicPlatform(EditorUserBuildSettings.activePlatformGuid);
- // profile can be null if we're in the middle of creating classic profiles
- if (buildProfile == null)
- return;
+ // profile can be null if we're in the middle of creating classic profiles
+ if (buildProfile == null)
+ return;
- // We only copy EditorUserBuildSettings into the build profile for classic platforms as we don't want to modify actual user assets
- EditorUserBuildSettings.CopyToBuildProfile(buildProfile);
- }
+ // We only copy EditorUserBuildSettings into the build profile for classic platforms as we don't want to modify actual user assets
+ EditorUserBuildSettings.CopyToBuildProfile(buildProfile);
var extension = ModuleManager.GetBuildProfileExtension(buildProfile.platformGuid);
if (extension != null)
diff --git a/Editor/Mono/BuildProfile/BuildProfileGraphicsSettingsEditor.cs b/Editor/Mono/BuildProfile/BuildProfileGraphicsSettingsEditor.cs
index 09cbac3d80..6b56237d0d 100644
--- a/Editor/Mono/BuildProfile/BuildProfileGraphicsSettingsEditor.cs
+++ b/Editor/Mono/BuildProfile/BuildProfileGraphicsSettingsEditor.cs
@@ -75,11 +75,8 @@ void BindEnumFieldWithFadeGroup(VisualElement content, string id, Action buttonC
var enumModeGroup = content.MandatoryQ($"{id}ModesGroup");
var enumModeProperty = serializedObject.FindProperty($"m_{id}Stripping");
- static bool IsModesGroupVisible(StrippingModes mode) => mode == StrippingModes.Custom;
- UIElementsEditorUtility.SetVisibility(enumModeGroup, IsModesGroupVisible((StrippingModes)enumModeProperty.intValue));
- var lightmapModesUpdate = UIElementsEditorUtility.BindSerializedProperty(enumMode, enumModeProperty,
- mode => UIElementsEditorUtility.SetVisibility(enumModeGroup, IsModesGroupVisible(mode)));
- lightmapModesUpdate?.Invoke();
+ UIElementsEditorUtility.BindSerializedProperty(enumMode, enumModeProperty,
+ mode => UIElementsEditorUtility.SetVisibility(enumModeGroup, mode == StrippingModes.Custom));
content.MandatoryQ($"Import{id}FromCurrentScene").clicked += buttonCallback;
}
diff --git a/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs b/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs
index 02aa23fd8d..41b42824cf 100644
--- a/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs
+++ b/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs
@@ -424,6 +424,41 @@ public static string GetModuleName(GUID platformId)
return BuildTargetDiscovery.GetModuleNameForBuildTarget(buildTarget);
}
+ const string k_WindowsArchitecturePlatformSetting = "Architecture";
+
+ ///
+ /// Resolves the active build target when activating a build profile. Windows architecture
+ /// (x86/x64/ARM64) is not encoded in the GUID or , so for
+ /// Windows standalone targets this maps the architecture platform setting onto the matching
+ /// target (x86 -> ).
+ ///
+ internal static BuildTarget GetActiveBuildTargetForProfileSwitch(BuildProfile profile, BuildTarget buildTargetFromGuid)
+ {
+ if (profile?.platformBuildProfile == null)
+ return buildTargetFromGuid;
+
+ if (!IsWindowsStandaloneBuildTarget(buildTargetFromGuid))
+ return buildTargetFromGuid;
+
+ var architecture = profile.platformBuildProfile.GetRawPlatformSetting(k_WindowsArchitecturePlatformSetting);
+ if (string.IsNullOrEmpty(architecture))
+ return buildTargetFromGuid;
+
+ switch (architecture.ToLowerInvariant())
+ {
+ case "x86":
+ return BuildTarget.StandaloneWindows;
+ case "x64":
+ case "arm64":
+ return BuildTarget.StandaloneWindows64;
+ default:
+ return buildTargetFromGuid;
+ }
+ }
+
+ static bool IsWindowsStandaloneBuildTarget(BuildTarget buildTarget) =>
+ buildTarget == BuildTarget.StandaloneWindows || buildTarget == BuildTarget.StandaloneWindows64;
+
///
/// Internal method for switching active build target and subtarget.
///
@@ -1102,6 +1137,13 @@ public static string GetPlatformColorString(GUID platformGuid)
return BuildTargetDiscovery.GetPlatformColorString(platformGuid);
}
+ ///
+ /// When the platform is marked deprecated, returns true and sets to the
+ /// configured text. The message may be empty if no custom copy was provided; callers should supply a fallback in that case.
+ ///
+ public static bool BuildPlatformTryGetDeprecationMessage(GUID platformGuid, out string deprecationMessage) =>
+ BuildTargetDiscovery.BuildPlatformTryGetDeprecationMessage(platformGuid, out deprecationMessage);
+
public static void OnActiveProfileGraphicsSettingsChanged(bool hasGraphicsSettings)
{
EditorGraphicsSettings.activeProfileHasGraphicsSettings = hasGraphicsSettings;
diff --git a/Editor/Mono/BuildProfile/BuildProfileRenameOverlay.cs b/Editor/Mono/BuildProfile/BuildProfileRenameOverlay.cs
index 71293ee1d5..6f4f789075 100644
--- a/Editor/Mono/BuildProfile/BuildProfileRenameOverlay.cs
+++ b/Editor/Mono/BuildProfile/BuildProfileRenameOverlay.cs
@@ -2,6 +2,7 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using System;
using UnityEngine;
using UnityEngine.Bindings;
using UnityEngine.UIElements;
@@ -49,20 +50,24 @@ public void OnNameChanged(string previousValue, string newValue)
// automatically on hover. So we use the TooltipView to display the
// error message (same way as the RenameOverlay used for assets)
TooltipView.Show(k_ErrorMessage, errorRect);
- m_TextField.SetValueWithoutNotify(previousValue);
// The cursor should be kept in place when adding an invalid character
+ // cursorIndex is clamped to the current text length, so read it before reverting,
+ // while the length still includes the invalid char, that keeps an end-of-string
+ // cursorIndex from being clamped. The '- 1' then undoes the inserted char's advance for
+ // both end-of-string and middle edits (middle edits are never clamped either way).
var targetIndex = Mathf.Max(m_TextField.cursorIndex - 1, 0);
+ m_TextField.SetValueWithoutNotify(previousValue);
m_TextField.cursorIndex = targetIndex;
m_TextField.selectIndex = targetIndex;
}
else if (System.Text.Encoding.UTF8.GetByteCount(newValue) > BuildProfileModuleUtil.k_MaxAssetFileNameLengthWithoutExtension)
{
TooltipView.Show(k_ErrorMessageLength, errorRect);
- m_TextField.SetValueWithoutNotify(previousValue);
// The cursor should be kept in place when adding too much
var targetIndex = Mathf.Max(m_TextField.cursorIndex - 1, 0);
+ m_TextField.SetValueWithoutNotify(previousValue);
m_TextField.cursorIndex = targetIndex;
m_TextField.selectIndex = targetIndex;
}
diff --git a/Editor/Mono/Commands/GOCreationCommands.cs b/Editor/Mono/Commands/GOCreationCommands.cs
index 4b759f77d2..13e806ae38 100644
--- a/Editor/Mono/Commands/GOCreationCommands.cs
+++ b/Editor/Mono/Commands/GOCreationCommands.cs
@@ -178,103 +178,108 @@ internal static void CreateEmptyParent()
GameObject defaultParentObject = SceneView.GetDefaultParentObjectIfSet()?.gameObject;
var defaultParentObjectScene = defaultParentObject != null ? defaultParentObject.scene : default;
- // Clear default parent object so we could always reparent and move the new parent to the scene we need
+ // Clear default parent object so the ObjectFactory does not attach the new parent to it.
if (defaultParentObject != null)
{
- SceneHierarchy.ClearDefaultParentObject(defaultParentObjectScene);
+ defaultParentObjectScene.defaultParent = EntityId.None;
}
- // If selected object is a prefab, get the its root object
- if (selected.Length > 0)
+ try
{
- for (int i = 0; i < selected.Length; i++)
+ // If selected object is a prefab, get the its root object
+ if (selected.Length > 0)
{
- if (PrefabUtility.GetPrefabAssetType(selected[i].gameObject) != PrefabAssetType.NotAPrefab)
+ for (int i = 0; i < selected.Length; i++)
{
- selected[i] = PrefabUtility.GetOutermostPrefabInstanceRoot(selected[i].gameObject).transform;
+ if (PrefabUtility.GetPrefabAssetType(selected[i].gameObject) != PrefabAssetType.NotAPrefab)
+ {
+ selected[i] = PrefabUtility.GetOutermostPrefabInstanceRoot(selected[i].gameObject).transform;
+ }
}
}
- }
- // Selection.transform does not provide correct list order, so we have to do it manually
- Array.Sort(selected, (a, b) => { return a.GetSiblingIndex().CompareTo(b.GetSiblingIndex()); });
+ // Selection.transform does not provide correct list order, so we have to do it manually
+ Array.Sort(selected, (a, b) => { return a.GetSiblingIndex().CompareTo(b.GetSiblingIndex()); });
- GameObject go = ObjectFactory.CreateGameObject("GameObject");
+ GameObject go = ObjectFactory.CreateGameObject("GameObject");
- if (Selection.activeGameObject == null && Selection.gameObjects != null)
- {
- Selection.activeGameObject = Selection.gameObjects[0];
- }
+ if (Selection.activeGameObject == null && Selection.gameObjects != null)
+ {
+ Selection.activeGameObject = Selection.gameObjects[0];
+ }
- if (Selection.activeGameObject != null)
- go.transform.position = Selection.activeGameObject.transform.position;
+ if (Selection.activeGameObject != null)
+ go.transform.position = Selection.activeGameObject.transform.position;
- GameObject parent = Selection.activeTransform != null ? Selection.activeTransform.gameObject : null;
- Transform sibling = null;
+ GameObject parent = Selection.activeTransform != null ? Selection.activeTransform.gameObject : null;
+ Transform sibling = null;
- if (parent != null)
- {
- sibling = parent.transform;
- parent = parent.transform.parent != null ? parent.transform.parent.gameObject : null;
- }
+ if (parent != null)
+ {
+ sibling = parent.transform;
+ parent = parent.transform.parent != null ? parent.transform.parent.gameObject : null;
+ }
- Place(go, parent, false);
- var rectTransform = go.GetComponent();
+ Place(go, parent, false);
+ var rectTransform = go.GetComponent();
- // If new parent is RectTransform, make sure its position and size matches child rect transforms
- if (rectTransform != null && selected != null && selected.Length > 0)
- {
- CenterRectTransform(selected, rectTransform);
- }
+ // If new parent is RectTransform, make sure its position and size matches child rect transforms
+ if (rectTransform != null && selected != null && selected.Length > 0)
+ {
+ CenterRectTransform(selected, rectTransform);
+ }
- if (parent == null && sibling != null)
- {
- Undo.MoveGameObjectToScene(go, sibling.gameObject.scene, "Move To Scene");
- }
+ if (parent == null && sibling != null)
+ {
+ Undo.MoveGameObjectToScene(go, sibling.gameObject.scene, "Move To Scene");
+ }
- if (parent == null && sibling == null)
- {
- go.transform.SetAsLastSibling();
- }
- else
- {
- go.transform.MoveAfterSibling(sibling, true);
- }
+ if (parent == null && sibling == null)
+ {
+ go.transform.SetAsLastSibling();
+ }
+ else
+ {
+ go.transform.MoveAfterSibling(sibling, true);
+ }
- // At this point, RecordStructureChange is already ongoing (from the CreateGameObject call).
- // We need to flush the stack to finalise the RecordStructureChange before any of following SetTransformParent calls takes place.
- Undo.FlushTrackedObjects();
+ // At this point, RecordStructureChange is already ongoing (from the CreateGameObject call).
+ // We need to flush the stack to finalise the RecordStructureChange before any of following SetTransformParent calls takes place.
+ Undo.FlushTrackedObjects();
- // Put gameObjects under a created parent
- if (selected.Length > 0)
- {
- foreach (var gameObject in selected)
+ // Put gameObjects under a created parent
+ if (selected.Length > 0)
{
- if (gameObject != null)
+ foreach (var gameObject in selected)
{
- Undo.SetTransformParent(gameObject.transform, go.transform, "Reparenting");
- gameObject.transform.SetAsLastSibling();
+ if (gameObject != null)
+ {
+ Undo.SetTransformParent(gameObject.transform, go.transform, "Reparenting");
+ gameObject.transform.SetAsLastSibling();
+ }
}
- }
- using var _ = ListPool.Get(out var windows);
- IHierarchyWindow.GetAllHierarchyWindows(windows);
- foreach (var window in windows)
- {
- window.SetExpanded(go.GetEntityId(), true);
- }
+ using var _ = ListPool.Get(out var windows);
+ IHierarchyWindow.GetAllHierarchyWindows(windows);
+ foreach (var window in windows)
+ {
+ window.SetExpanded(go.GetEntityId(), true);
+ }
- // Ensure empty parent after reparenting jumps into rename mode if needed UUM-15042
- if (HierarchyPreferences.RenameNewObjects)
- {
- SceneHierarchyWindow.FrameAndRenameNewGameObject();
+ // Ensure empty parent after reparenting jumps into rename mode if needed UUM-15042
+ if (HierarchyPreferences.RenameNewObjects)
+ {
+ SceneHierarchyWindow.FrameAndRenameNewGameObject();
+ }
}
}
-
- // Set back default parent object if we have one
- if (defaultParentObject != null)
+ finally
{
- defaultParentObjectScene.defaultParent = defaultParentObject.GetEntityId();
+ // Set back default parent object if we have one, even if creation failed above.
+ if (defaultParentObject != null)
+ {
+ defaultParentObjectScene.defaultParent = defaultParentObject.GetEntityId();
+ }
}
}
diff --git a/Editor/Mono/CutBoard.cs b/Editor/Mono/CutBoard.cs
index 5a50f8947d..1a6fb2e024 100644
--- a/Editor/Mono/CutBoard.cs
+++ b/Editor/Mono/CutBoard.cs
@@ -15,6 +15,9 @@ namespace UnityEditor
internal static class CutBoard
{
internal static bool hasCutboardData { get { return m_GOCutboard != null && m_GOCutboard.Length > 0; } }
+ internal static ReadOnlySpan cutTransformsSpan => m_GOCutboard;
+ internal static event Action cleared;
+
private static Transform[] m_GOCutboard;
private static Object[] m_SelectedObjects;
private static HashSet m_CutAffectedGOs = new HashSet();
@@ -188,9 +191,12 @@ internal static bool IsGameObjectPartOfCutAndPaste(GameObject gameObject)
internal static void Reset()
{
+ var hadCutboardData = hasCutboardData;
m_SelectedObjects = null;
m_GOCutboard = null;
m_CutAffectedGOs.Clear();
+ if (hadCutboardData)
+ cleared?.Invoke();
}
internal static bool AreCutAndPasteStagesSame()
diff --git a/Editor/Mono/EditorApplication.cs b/Editor/Mono/EditorApplication.cs
index 324072d1f1..4508bf4633 100644
--- a/Editor/Mono/EditorApplication.cs
+++ b/Editor/Mono/EditorApplication.cs
@@ -506,18 +506,23 @@ static void Internal_PauseStateChanged(PauseState state)
[RequiredByNativeCode]
static void Internal_EnterEditModeLifecycleScope()
{
+ // EditModeScope can already be active when this is called (UUM-148454):
+ // with "Script Changes While Playing" set to "Recompile After Finished Playing",
+ // a script edited during play mode is compiled while the assembly reload is locked.
+ // On stop, ExitPlayMode() unlocks the reload and runs AutoRefresh(), which performs
+ // the pending domain reload synchronously before the EnteredEditMode notification
+ // fires. After that reload EditModeScopePostprocessor.OnPostprocessAllAssets has
+ // already re-entered EditModeScope, so entering it here again must be skipped.
if (Unity.Scripting.LifecycleManagement.LifecycleController.Instance.IsScopePresent())
return;
- if (!Unity.Scripting.LifecycleManagement.LifecycleController.Instance.IsScopePresent())
- Unity.Scripting.LifecycleManagement.LifecycleController.Instance.EnterScope();
-
Unity.Scripting.LifecycleManagement.LifecycleController.Instance.EnterScope();
}
[RequiredByNativeCode]
static void Internal_ExitEditModeLifecycleScope()
{
+ // This method can be called while not in EditModeScope if we exit the editor while in PlayModeScope
if (!Unity.Scripting.LifecycleManagement.LifecycleController.Instance.IsScopePresent())
return;
diff --git a/Editor/Mono/EditorAssemblies.cs b/Editor/Mono/EditorAssemblies.cs
index f2b1e87f7d..7eff5daf73 100644
--- a/Editor/Mono/EditorAssemblies.cs
+++ b/Editor/Mono/EditorAssemblies.cs
@@ -115,40 +115,27 @@ private static void ValidateSourceGenerators(Assembly[] assemblies)
static ProfilerMarkerWithStringData _profilerMarkerProcessInitializeOnLoadAttributes = ProfilerMarkerWithStringData.Create("ProcessInitializeOnLoadAttribute", "Type");
static ProfilerMarkerWithStringData _profilerMarkerProcessInitializeOnLoadMethodAttributes = ProfilerMarkerWithStringData.Create("ProcessInitializeOnLoadMethodAttribute", "MethodInfo");
- private static readonly ProfilerMarker _profilerMarkerSortTypes = new ProfilerMarker("SortTypesTopologically");
+ // Native (InitializeOnLoadOrdering::OrderTypesByAssembly) hands the handles over already ordered by assembly.
[RequiredByNativeCode]
private static void ProcessInitializeOnLoadAttributes(ReadOnlySpan typeHandles)
{
if (typeHandles.Length == 0)
return;
- var types = SystemReflectionMarshalling.UnmarshalSystemTypes(typeHandles);
-
bool reportTimes = (bool)Debug.GetDiagnosticSwitch("EnableDomainReloadTimings").value;
- IEnumerable sortedTypes;
- using (_profilerMarkerSortTypes.Auto())
- {
- // Sort types according to the list of loaded assemblies (which are topologically-sorted), such that we guarantee that
- // [InitializeOnLoad] classes in assemblies referenced by a given assembly will have been
- // initialized prior to that assembly's own [InitializeOnLoad] classes.
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- sortedTypes = types.OrderBy(x => Array.IndexOf(loadedAssemblies, x.Assembly));
-#pragma warning restore UA2001
- }
-
using var scope = new ProgressScope("Running managed callbacks", "Initializing InitializeOnLoad Types", forceUpdate: true);
- foreach (Type type in sortedTypes)
+ foreach (var typeHandlePtr in typeHandles)
{
+ var typeHandle = SystemReflectionMarshalling.UnmarshalRuntimeTypeHandle(typeHandlePtr);
using (_profilerMarkerProcessInitializeOnLoadAttributes.Auto(reportTimes,
- () => type.AssemblyQualifiedName))
+ () => Type.GetTypeFromHandle(typeHandle).AssemblyQualifiedName))
{
- var typeFullName = type?.FullName;
try
{
- RuntimeHelpers.RunClassConstructor(type.TypeHandle);
+ RuntimeHelpers.RunClassConstructor(typeHandle);
}
catch (TypeLoadException x)
{
diff --git a/Editor/Mono/EditorGUI.cs b/Editor/Mono/EditorGUI.cs
index 88ea1f5eac..6714fbcba3 100644
--- a/Editor/Mono/EditorGUI.cs
+++ b/Editor/Mono/EditorGUI.cs
@@ -2283,10 +2283,7 @@ internal static void FloatFieldInternal(Rect position, GUIContent label, ref Num
int id = GUIUtility.GetControlID(s_FloatFieldHash, FocusType.Keyboard, position);
Rect position2 = PrefixLabel(position, id, label);
position.xMax = position2.x;
- var dragSensitivity = Event.current.GetTypeForControl(id) == EventType.MouseDown
- ? (float)NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue)
- : 0.0f;
- DoNumberField(s_RecycledEditor, position2, position, id, ref value, kFloatFieldFormatString, style, true, dragSensitivity);
+ DoNumberField(s_RecycledEditor, position2, position, id, ref value, kFloatFieldFormatString, style, true);
}
internal static double DoubleFieldInternal(Rect position, double value, GUIStyle style)
@@ -2308,10 +2305,7 @@ internal static void DoubleFieldInternal(Rect position, GUIContent label, ref Nu
int id = GUIUtility.GetControlID(s_FloatFieldHash, FocusType.Keyboard, position);
Rect position2 = PrefixLabel(position, id, label);
position.xMax = position2.x;
- var dragSensitivity = Event.current.GetTypeForControl(id) == EventType.MouseDown
- ? NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue)
- : 0.0;
- DoNumberField(s_RecycledEditor, position2, position, id, ref value, kDoubleFieldFormatString, style, true, dragSensitivity);
+ DoNumberField(s_RecycledEditor, position2, position, id, ref value, kDoubleFieldFormatString, style, true);
}
// Handle dragging of value
@@ -2351,6 +2345,13 @@ static void DragNumberValue(Rect dragHotZone, int id, ref NumberFieldValue value
s_DragStartIntValue = value.longVal;
s_DragStartPos = evt.mousePosition;
s_DragSensitivity = dragSensitivity;
+
+ // NaN means the caller wants the default sensitivity.
+ if (double.IsNaN(s_DragSensitivity))
+ {
+ s_DragSensitivity = value.isDouble ? NumericFieldDraggerUtility.CalculateFloatDragSensitivity(value.doubleVal) : NumericFieldDraggerUtility.CalculateIntDragSensitivity(value.longVal);
+ }
+
evt.Use();
EditorGUIUtility.SetWantsMouseJumping(1);
}
@@ -2418,7 +2419,7 @@ static void DragNumberValue(Rect dragHotZone, int id, ref NumberFieldValue value
internal static float DoFloatField(RecycledTextEditor editor, Rect position, Rect dragHotZone, int id, float value, string formatString, GUIStyle style, bool draggable)
{
- return DoFloatField(editor, position, dragHotZone, id, value, formatString, style, draggable, Event.current.GetTypeForControl(id) == EventType.MouseDown ? (float)NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue) : 0.0f);
+ return DoFloatField(editor, position, dragHotZone, id, value, formatString, style, draggable, float.NaN);
}
internal static float DoFloatField(RecycledTextEditor editor, Rect position, Rect dragHotZone, int id, float value, string formatString, GUIStyle style, bool draggable, float dragSensitivity)
@@ -2440,7 +2441,7 @@ internal static int DoIntField(RecycledTextEditor editor, Rect position, Rect dr
internal static double DoDoubleField(RecycledTextEditor editor, Rect position, Rect dragHotZone, int id, double value, string formatString, GUIStyle style, bool draggable)
{
- return DoDoubleField(editor, position, dragHotZone, id, value, formatString, style, draggable, Event.current.GetTypeForControl(id) == EventType.MouseDown ? NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue) : 0.0);
+ return DoDoubleField(editor, position, dragHotZone, id, value, formatString, style, draggable, double.NaN);
}
internal static double DoDoubleField(RecycledTextEditor editor, Rect position, Rect dragHotZone, int id, double value, string formatString, GUIStyle style, bool draggable, double dragSensitivity)
@@ -2574,7 +2575,7 @@ internal static void UpdateNumberValueIfNeeded(ref NumberFieldValue value, in st
}
internal static void DoNumberField(RecycledTextEditor editor, Rect position, Rect dragHotZone, int id,
ref NumberFieldValue value, string formatString, GUIStyle style, bool draggable,
- double dragSensitivity)
+ double dragSensitivity = double.NaN)
{
bool changed;
string allowedCharacters = value.isDouble ? s_AllowedCharactersForFloat : s_AllowedCharactersForInt;
@@ -2656,32 +2657,42 @@ internal static int ArraySizeField(Rect position, GUIContent label, int value, G
string str = DelayedTextFieldInternal(position, id, label, value.ToString(kIntFieldFormatString), "0123456789-", style);
if (EndChangeCheck())
{
- if (!int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out int newValue))
+ if (!TryConfirmArraySizeChange(value, str, out int newValue))
{
- EditorUtility.DisplayDialog(
- L10n.Tr("Invalid array size"),
- string.Format(L10n.Tr("\"{0}\" is not a valid array size. Allowed values are between 0 and {1:N0}."), str, int.MaxValue),
- L10n.Tr("OK"));
GUI.changed = wasChanged;
return value;
}
- if (newValue > kArraySizeConfirmationThreshold && newValue > value)
- {
- if (!EditorUtility.DisplayDialog(
- L10n.Tr("Resize array"),
- string.Format(L10n.Tr("You are about to resize this array to {0:N0} elements. This may take a long time and use a lot of memory. Are you sure?"), newValue),
- L10n.Tr("Resize"),
- L10n.Tr("Cancel")))
- {
- GUI.changed = wasChanged;
- return value;
- }
- }
value = newValue;
}
return value;
}
+ internal static bool TryConfirmArraySizeChange(int currentSize, string str, out int newSize)
+ {
+ if (!int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out newSize))
+ {
+ EditorUtility.DisplayDialog(
+ L10n.Tr("Invalid array size"),
+ string.Format(L10n.Tr("\"{0}\" is not a valid array size. Allowed values are between 0 and {1:N0}."), str, int.MaxValue),
+ L10n.Tr("OK"));
+ newSize = currentSize;
+ return false;
+ }
+ if (newSize > kArraySizeConfirmationThreshold && newSize > currentSize)
+ {
+ if (!EditorUtility.DisplayDialog(
+ L10n.Tr("Resize array"),
+ string.Format(L10n.Tr("You are about to resize this array to {0:N0} elements. This may take a long time and use a lot of memory. Are you sure?"), newSize),
+ L10n.Tr("Resize"),
+ L10n.Tr("Cancel")))
+ {
+ newSize = currentSize;
+ return false;
+ }
+ }
+ return true;
+ }
+
internal static string DelayedTextFieldInternal(Rect position, string value, string allowedLetters, GUIStyle style)
{
int id = GUIUtility.GetControlID(s_DelayedTextFieldHash, FocusType.Keyboard, position);
@@ -2764,7 +2775,7 @@ internal static void DelayedTextFieldInternal(Rect position, int id, SerializedP
internal static void DelayedNumberFieldInternal(Rect position, Rect dragHotZone, int id, bool isDouble,
ref double doubleVal, ref long longVal, string formatString, GUIStyle style, bool draggable,
- double dragSensitivity)
+ double dragSensitivity = double.NaN)
{
NumberFieldValue val = default;
val.isDouble = isDouble;
@@ -2874,7 +2885,7 @@ internal static float DelayedFloatFieldInternal(Rect position, GUIContent label,
bool draggable = SetDelayedDraggable(ref position, ref dragHotzone, label, id);
BeginChangeCheck();
- DelayedNumberFieldInternal(position, dragHotzone, id, true, ref doubleValue, ref dummy, kFloatFieldFormatString, style, draggable, Event.current.GetTypeForControl(id) == EventType.MouseDown ? (float)NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue) : 0.0f);
+ DelayedNumberFieldInternal(position, dragHotzone, id, true, ref doubleValue, ref dummy, kFloatFieldFormatString, style, draggable);
if (EndChangeCheck())
{
if ((float)doubleValue != value)
@@ -2907,7 +2918,7 @@ internal static double DelayedDoubleFieldInternal(Rect position, GUIContent labe
bool draggable = SetDelayedDraggable(ref position, ref dragHotzone, label, id);
BeginChangeCheck();
- DelayedNumberFieldInternal(position, dragHotzone, id, true, ref newDoubleValue, ref dummy, kFloatFieldFormatString, style, draggable, Event.current.GetTypeForControl(id) == EventType.MouseDown ? (float)NumericFieldDraggerUtility.CalculateFloatDragSensitivity(s_DragStartValue) : 0.0f);
+ DelayedNumberFieldInternal(position, dragHotzone, id, true, ref newDoubleValue, ref dummy, kFloatFieldFormatString, style, draggable);
if (EndChangeCheck())
{
if (newDoubleValue != value)
@@ -4958,7 +4969,13 @@ private static void QuaternionEulerField(Rect position, SerializedProperty prope
MultiFloatFieldInternal(position, s_XYZLabels, s_Vector3Floats);
if (EndChangeCheck())
{
- property.quaternionValue = Quaternion.Euler(s_Vector3Floats[0], s_Vector3Floats[1], s_Vector3Floats[2]);
+ bool isFinite = (!float.IsNaN(s_Vector3Floats[0]) && !float.IsInfinity(s_Vector3Floats[0]) &&
+ !float.IsNaN(s_Vector3Floats[1]) && !float.IsInfinity(s_Vector3Floats[1]) &&
+ !float.IsNaN(s_Vector3Floats[2]) && !float.IsInfinity(s_Vector3Floats[2]));
+ if (isFinite)
+ {
+ property.quaternionValue = Quaternion.Euler(s_Vector3Floats[0], s_Vector3Floats[1], s_Vector3Floats[2]);
+ }
}
}
@@ -6171,9 +6188,23 @@ internal static bool EnableCheckBoxInTitlebar(Object targetObj)
return true;
}
+ // A missing script is fake-null but its native object still exists, so keep drawing its titlebar
+ // and Remove Component menu. A destroyed target has no native object and throws on access, so skip it. (UUM-146874)
+ internal static bool ShouldSkipInspectorTitlebar(Object target)
+ {
+ if (target != null)
+ return false;
+ if ((object)target == null)
+ return true;
+ return !Resources.EntityIdIsValid(target.GetEntityId());
+ }
+
// Make an inspector-window-like titlebar.
internal static void DoInspectorTitlebar(Rect position, int id, bool foldout, Object[] targetObjs, SerializedProperty enabledProperty, GUIStyle baseStyle)
{
+ if (ShouldSkipInspectorTitlebar(targetObjs[0]))
+ return;
+
GUIStyle textStyle = EditorStyles.inspectorTitlebarText;
GUIStyle iconButtonStyle = EditorStyles.iconButton;
Event evt = Event.current;
diff --git a/Editor/Mono/EditorGUIUtility.cs b/Editor/Mono/EditorGUIUtility.cs
index 1246d7660a..5e0891c7e1 100644
--- a/Editor/Mono/EditorGUIUtility.cs
+++ b/Editor/Mono/EditorGUIUtility.cs
@@ -28,6 +28,7 @@ namespace UnityEditor
{
public sealed partial class EditorGUIUtility : GUIUtility
{
+ [VisibleToOtherModules("UnityEditor.VectorGraphicsModule")]
internal static void RegisterResourceForCleanupOnDomainReload(UnityObject obj)
{
#pragma warning disable UAC0006 // CORECLR_FIXME: CoreCLR would handle this using OnCodeUnloading/OnCodeLoaded
diff --git a/Editor/Mono/EditorMode/MenuService.cs b/Editor/Mono/EditorMode/MenuService.cs
index c2054b193c..2514f8dae3 100644
--- a/Editor/Mono/EditorMode/MenuService.cs
+++ b/Editor/Mono/EditorMode/MenuService.cs
@@ -16,18 +16,21 @@
using static UnityEditor.ModeService;
using UnityEditor.Scripting.ScriptCompilation;
using UnityEditor.ShortcutManagement;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
- static class MenuService
+ static partial class MenuService
{
private const string k_WindowMenuName = "Window";
private const string k_HelpMenuName = "Help";
// Contains the final ordered menus that can come either from the .mode file or from attributes
- private static readonly Dictionary> s_MenusFromModeFile = new Dictionary>();
+ [AutoStaticsCleanupOnCodeReload]
+ private static Dictionary> s_MenusFromModeFile = new Dictionary>();
// Contains menu from attributes for modes other than default
// Used to add menu items from attributes when iterating through the .mode menus
+ [AutoStaticsCleanupOnCodeReload]
private static Dictionary> s_MenuItemsPerMode = null;
// Contains menu from attributes for the default mode
// The default mode menus are in a separate dictionary for performance reasons, in that case we don't need the costly MenuItemsTree structure because it won't be used when iterating through the .mode menus
@@ -46,6 +49,7 @@ public GroupingMenuItemScriptCommand(string menuName, MenuItemScriptCommand mi)
public string menuPath;
}
+ [AutoStaticsCleanupOnCodeReload]
private static Dictionary s_MenuItemsDefaultMode = null;
[UsedImplicitly, RequiredByNativeCode]
diff --git a/Editor/Mono/EditorPrefs.bindings.cs b/Editor/Mono/EditorPrefs.bindings.cs
index 3c4bbb922d..e0a1e160df 100644
--- a/Editor/Mono/EditorPrefs.bindings.cs
+++ b/Editor/Mono/EditorPrefs.bindings.cs
@@ -90,6 +90,7 @@ public static bool GetBool(string key)
// Removes all keys and values from the preferences. Use with caution.
public static extern void DeleteAll();
+ [VisibleToOtherModules("MultiplayerEditorModule")]
internal static extern void Sync();
internal static extern string[] GetKeys();
}
diff --git a/Editor/Mono/EditorSettings.bindings.cs b/Editor/Mono/EditorSettings.bindings.cs
index 6699d39a9a..7b3e83ce90 100644
--- a/Editor/Mono/EditorSettings.bindings.cs
+++ b/Editor/Mono/EditorSettings.bindings.cs
@@ -343,6 +343,27 @@ public enum NamingScheme
[StaticAccessor("GetEditorSettings()", StaticAccessorType.Dot)]
internal static extern bool inspectorUseIMGUIDefaultInspector { get; set; }
+ [StaticAccessor("GetEditorSettings()", StaticAccessorType.Dot)]
+ [NativeName("UseLegacyHierarchy")]
+ private static extern bool useLegacyHierarchyImpl { get; set; }
+
+ [VisibleToOtherModules]
+ internal static bool useLegacyHierarchy
+ {
+ get => useLegacyHierarchyImpl;
+ set
+ {
+ if (useLegacyHierarchyImpl == value)
+ return;
+
+ useLegacyHierarchyImpl = value;
+ useLegacyHierarchyChanged?.Invoke();
+ }
+ }
+
+ [VisibleToOtherModules]
+ internal static Action useLegacyHierarchyChanged;
+
[StaticAccessor("GetEditorSettings()", StaticAccessorType.Dot)]
public static extern bool referencedClipsExactNaming { get; set; }
diff --git a/Editor/Mono/EditorUserBuildSettings.bindings.cs b/Editor/Mono/EditorUserBuildSettings.bindings.cs
index 83183a4206..223a612b65 100644
--- a/Editor/Mono/EditorUserBuildSettings.bindings.cs
+++ b/Editor/Mono/EditorUserBuildSettings.bindings.cs
@@ -396,6 +396,9 @@ public static BuildTarget selectedStandaloneTarget
break;
}
+ if (internal_SelectedStandaloneTarget == value)
+ return;
+
internal_SelectedStandaloneTarget = value;
}
}
@@ -850,6 +853,8 @@ internal static bool SwitchActiveBuildTargetGuid(BuildProfile profile)
if (subtarget != -1) activeSubtarget = subtarget;
}
+ buildTargetFromGuid = BuildProfileModuleUtil.GetActiveBuildTargetForProfileSwitch(profile, buildTargetFromGuid);
+
return SwitchActiveBuildTargetAndSubTargetGuid(profile.platformGuid, buildTargetFromGuid, activeSubtarget);
}
diff --git a/Editor/Mono/EditorUserSettings.bindings.cs b/Editor/Mono/EditorUserSettings.bindings.cs
index 90e2543a46..8cfc801662 100644
--- a/Editor/Mono/EditorUserSettings.bindings.cs
+++ b/Editor/Mono/EditorUserSettings.bindings.cs
@@ -64,6 +64,9 @@ public static string GetConfigValue(string name)
[NativeProperty("VCScanLocalPackagesOnConnect")]
public static extern bool scanLocalPackagesOnConnect { get; set; }
+ [NativeProperty("VCAutoRevertUnchangedFiles")]
+ public static extern bool autoRevertUnchangedFiles { get; set; }
+
[NativeProperty("VCDebugCmd")]
internal static extern bool DebugCmd { get; set; }
diff --git a/Editor/Mono/EditorWindow.cs b/Editor/Mono/EditorWindow.cs
index 3cd40244bc..b0f39b3848 100644
--- a/Editor/Mono/EditorWindow.cs
+++ b/Editor/Mono/EditorWindow.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System;
using System.Collections.Generic;
+using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEditor.ShortcutManagement;
using UnityEngine.Scripting;
@@ -94,6 +95,12 @@ public VisualElement rootVisualElement
[NonSerialized]
bool m_OverlaysInitialized;
+ [NonSerialized]
+ bool m_EditorToolsInitialized;
+
+ [NonSerialized]
+ VisualElement m_EditorToolsIMGUIContainer;
+
private bool m_EnableViewDataPersistence;
private bool m_RequestedViewDataSave;
@@ -1336,6 +1343,59 @@ void InitializeOverlayCanvas()
}
}
+ void InitializeEditorTools()
+ {
+ if (this is SceneView)
+ return;
+
+ if (this is ISupportsEditorTools && !m_EditorToolsInitialized)
+ {
+ m_EditorToolsIMGUIContainer = EditorToolUtility.CreateEditorToolsIMGUIContainer(this, OnEditorToolsContainerGUI);
+ rootVisualElement.Add(m_EditorToolsIMGUIContainer);
+ m_EditorToolsInitialized = true;
+ }
+ }
+
+ void OnEditorToolsContainerGUI()
+ {
+ var toolsWindow = (ISupportsEditorTools)this;
+ var handlesCamera = toolsWindow.handlesCamera;
+ if (handlesCamera == null)
+ return;
+
+ var worldBound = m_EditorToolsIMGUIContainer.worldBound;
+ if (worldBound.width <= 0 || worldBound.height <= 0)
+ return;
+
+ var prevPixelRect = handlesCamera.pixelRect;
+ var prevHandlesCamera = Handles.currentCamera;
+ try
+ {
+ var worldBoundInPixels = EditorGUIUtility.PointsToPixels(worldBound);
+ // Adjust pixelRect during Repaint if needed so the handles visual syncs
+ // with their logical position (they get offset when overlays are docked as toolbars).
+ if (Event.current.type == EventType.Repaint)
+ {
+ handlesCamera.pixelRect = new Rect(worldBoundInPixels.x, Screen.height - worldBoundInPixels.yMax,
+ worldBoundInPixels.width, worldBoundInPixels.height);
+ }
+ else
+ {
+ handlesCamera.pixelRect = new Rect(0, 0, worldBoundInPixels.width, worldBoundInPixels.height);
+ }
+
+ Handles.SetCamera(handlesCamera);
+
+ EditorToolManager.OnToolGUI(this);
+ }
+ finally
+ {
+ if (handlesCamera != null)
+ handlesCamera.pixelRect = prevPixelRect;
+ Handles.currentCamera = prevHandlesCamera;
+ }
+ }
+
void __internalAwake()
{
hideFlags = HideFlags.DontSave; // Can't be HideAndDontSave, as that would make scriptable wizard GUI be disabled
@@ -1352,6 +1412,7 @@ void OnEnableINTERNAL()
{
activeEditorWindows.Add(this);
InitializeOverlayCanvas();
+ InitializeEditorTools();
}
void OnDisableINTERNAL()
diff --git a/Editor/Mono/GI/BakePipeline.bindings.cs b/Editor/Mono/GI/BakePipeline.bindings.cs
index f09c147b83..11e01b5ac1 100644
--- a/Editor/Mono/GI/BakePipeline.bindings.cs
+++ b/Editor/Mono/GI/BakePipeline.bindings.cs
@@ -84,6 +84,7 @@ void Destroy()
extern void Update(bool isOnDemandBakeInProgress, bool isOnDemandBakeAsync, bool shouldBeRunning,
ref float progress, ref StageName currentStage);
extern bool RunInProgress();
+ extern void ClearProgress();
internal static class BindingsMarshaller
{
diff --git a/Editor/Mono/GI/InteractiveLightBaking.bindings.cs b/Editor/Mono/GI/InteractiveLightBaking.bindings.cs
index b3e797b42e..8c9b83ea93 100644
--- a/Editor/Mono/GI/InteractiveLightBaking.bindings.cs
+++ b/Editor/Mono/GI/InteractiveLightBaking.bindings.cs
@@ -48,6 +48,13 @@ public LightmapData[] ToLightmapData()
[NativeName("GetInteractiveLightingSettings")]
public static extern LightingSettings GetLightingSettings();
+ // False in processes without GI, such as the out of process profiler.
+ public static extern bool isAvailable
+ {
+ [FreeFunction("HasInteractiveLightBakingData")]
+ get;
+ }
+
[StaticAccessor("InteractiveLightBakingDataManager::Get()", StaticAccessorType.Dot)]
public static extern float lightmapResolutionScale { get; set; }
}
diff --git a/Editor/Mono/GI/LightmapEditorSettingsDeprecated.cs b/Editor/Mono/GI/LightmapEditorSettingsDeprecated.cs
index d121411236..8e45a85803 100644
--- a/Editor/Mono/GI/LightmapEditorSettingsDeprecated.cs
+++ b/Editor/Mono/GI/LightmapEditorSettingsDeprecated.cs
@@ -4,6 +4,7 @@
using UnityEngine;
using UnityEngineInternal;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
@@ -111,7 +112,9 @@ public static GIBakeBackend giBakeBackend
{
get
{
+#pragma warning disable 618 // ProgressiveCPU is deprecated; legacy giBakeBackend bridge still needs to map to it.
if (lightmapper == Lightmapper.ProgressiveCPU)
+#pragma warning restore 618
return GIBakeBackend.PathTracer;
else
return GIBakeBackend.Radiosity;
@@ -119,7 +122,9 @@ public static GIBakeBackend giBakeBackend
set
{
if (value == GIBakeBackend.PathTracer)
+#pragma warning disable 618 // ProgressiveCPU is deprecated; legacy giBakeBackend bridge still needs to map to it.
lightmapper = Lightmapper.ProgressiveCPU;
+#pragma warning restore 618
else
lightmapper = Lightmapper.Enlighten;
}
@@ -179,6 +184,7 @@ public static int maxAtlasWidth
set { maxAtlasSize = value; }
}
+ [AutoStaticsCleanupOnCodeReload]
private static int m_MaxAtlasHeight = 1024;
[System.Obsolete("LightmapEditorSettings.maxAtlasHeight has been deprecated. Only square atlases are supported, please use the maxAtlasSize instead. ")]
diff --git a/Editor/Mono/GI/Lightmapping.bindings.cs b/Editor/Mono/GI/Lightmapping.bindings.cs
index 7ae52a07ee..02397a755b 100644
--- a/Editor/Mono/GI/Lightmapping.bindings.cs
+++ b/Editor/Mono/GI/Lightmapping.bindings.cs
@@ -278,8 +278,10 @@ public static bool Bake()
public static extern bool isRunning {[FreeFunction("IsRunningLightmapping")] get; }
[System.Obsolete("OnStartedFunction.started is obsolete, please use bakeStarted instead. ", false)]
+ [AutoStaticsCleanupOnCodeReload]
public static event OnStartedFunction started;
+ [AutoStaticsCleanupOnCodeReload]
public static event Action bakeStarted;
private static void OpenNestedSubScenes()
@@ -323,6 +325,7 @@ private static void Internal_CallBakeStartedFunctions()
#pragma warning restore 0618
}
+ [AutoStaticsCleanupOnCodeReload]
internal static event Action startedRendering;
[RequiredByNativeCode]
@@ -332,6 +335,7 @@ internal static void Internal_CallStartedRenderingFunctions()
startedRendering();
}
+ [AutoStaticsCleanupOnCodeReload]
public static event Action lightingDataUpdated;
[RequiredByNativeCode]
@@ -341,6 +345,7 @@ internal static void Internal_CallLightingDataUpdatedFunctions()
lightingDataUpdated();
}
+ [AutoStaticsCleanupOnCodeReload]
public static event Action lightingDataCleared;
[RequiredByNativeCode]
@@ -350,6 +355,7 @@ internal static void Internal_CallLightingDataCleared()
lightingDataCleared();
}
+ [AutoStaticsCleanupOnCodeReload]
public static event Action lightingDataAssetCleared;
[RequiredByNativeCode]
@@ -359,6 +365,7 @@ internal static void Internal_CallLightingDataAssetCleared()
lightingDataAssetCleared();
}
+ [AutoStaticsCleanupOnCodeReload]
internal static event Action wroteLightingDataAsset;
[RequiredByNativeCode]
@@ -370,6 +377,7 @@ internal static void Internal_CallOnWroteLightingDataAsset()
// This event is fired when BakeInput has been populated, but before passing it to Bake().
// Do not store and access BakeInput beyond the call-back.
+ [AutoStaticsCleanupOnCodeReload]
internal static event Action createdBakeInput;
[RequiredByNativeCode]
@@ -386,8 +394,10 @@ internal static void Internal_CallOnCreatedBakeInput(IntPtr p_BakeInput, IntPtr
}
[System.Obsolete("OnCompletedFunction.completed is obsolete, please use event bakeCompleted instead. ", false)]
+ [AutoStaticsCleanupOnCodeReload]
public static OnCompletedFunction completed;
+ [AutoStaticsCleanupOnCodeReload]
public static event Action bakeCompleted;
[RequiredByNativeCode]
@@ -402,6 +412,7 @@ private static void Internal_CallBakeCompletedFunctions()
#pragma warning restore 0618
}
+ [AutoStaticsCleanupOnCodeReload]
public static event Action bakeCancelled;
[RequiredByNativeCode]
@@ -411,6 +422,7 @@ private static void Internal_CallBakeCancelledFunctions()
bakeCancelled();
}
+ [AutoStaticsCleanupOnCodeReload]
internal static event Action bakeAnalytics;
[RequiredByNativeCode]
@@ -729,8 +741,10 @@ internal static void AdditionalBake(ref float progress, ref bool done)
delegate void VirtualOffsetBakeUpdateDelegate(ref float progress, ref bool done);
[RequiredByNativeCode]
+ [AutoStaticsCleanupOnCodeReload]
static VirtualOffsetBakeInitializeDataDelegate s_VirtualOffsetBakeInitializeDataDelegate;
[RequiredByNativeCode]
+ [AutoStaticsCleanupOnCodeReload]
static VirtualOffsetBakeUpdateDelegate s_VirtualOffsetBakeUpdateDelegate;
static class VirtualOffsetBake
@@ -795,12 +809,14 @@ private static void Internal_CallBakeWithBakeInputFunctions(ref float progress,
}
[RequiredByNativeCode]
+ [NoAutoStaticsCleanup]
private static readonly AdditionalBakeDelegate s_DefaultAdditionalBakeDelegate = (ref float progress, ref bool done) =>
{
progress = 100.0f;
done = true;
};
[RequiredByNativeCode]
+ [AutoStaticsCleanupOnCodeReload]
private static AdditionalBakeDelegate s_AdditionalBakeDelegate = s_DefaultAdditionalBakeDelegate;
[RequiredByNativeCode]
diff --git a/Editor/Mono/GUI/AppStatusBar.cs b/Editor/Mono/GUI/AppStatusBar.cs
index 2972a09e21..dd35e90509 100644
--- a/Editor/Mono/GUI/AppStatusBar.cs
+++ b/Editor/Mono/GUI/AppStatusBar.cs
@@ -372,27 +372,58 @@ private void RefreshProgressBar(Progress.Item[] progressItems)
}
else
{
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- var currentItem = progressItems.FirstOrDefault(item => item.priority != (int)Progress.Priority.Idle);
-#pragma warning restore UA2001
- if (currentItem != null && !String.IsNullOrEmpty(currentItem.description))
- m_ProgressStatus.tooltip = currentItem.name + "\r\n" + currentItem.description;
- m_ProgressPercentageStatus.text = Progress.globalProgress.ToString("P", percentageFormat);
-
var remainingTimeText = "";
-#pragma warning disable UA2001 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- var runningProgresses = Progress.EnumerateItems().Where(item => item.running);
-#pragma warning restore UA2001
-#pragma warning disable UA2006 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- if (Progress.globalRemainingTime.TotalSeconds > 0 && runningProgresses.Any(item => item.timeDisplayMode == Progress.TimeDisplayMode.ShowRemainingTime && item.priority != (int)Progress.Priority.Idle) &&
-#pragma warning restore UA2006
-#pragma warning disable UA2008 // The Banned API Analyzer produces compile errors for any new Linq code. This pre-existing usage has been suppressed, but should be rewritten if possible.
- runningProgresses.All(item => !item.indefinite))
-#pragma warning restore UA2008
+
+ var anyShowRemainingTime = false;
+ var allDefinite = true;
+ var topLevelRunningCount = 0;
+ Progress.Item firstTopLevelItem = null;
+
+ foreach (var item in Progress.EnumerateItems())
+ {
+ if (item.running)
+ {
+ if (item.timeDisplayMode == Progress.TimeDisplayMode.ShowRemainingTime && item.priority != (int)Progress.Priority.Idle)
+ {
+ anyShowRemainingTime = true;
+ }
+ if (item.indefinite)
+ {
+ allDefinite = false;
+ }
+ if (item.parentId == -1 && item.priority != (int)Progress.Priority.Idle)
+ {
+ topLevelRunningCount++;
+ if (firstTopLevelItem == null)
+ {
+ firstTopLevelItem = item;
+ }
+ }
+ }
+ }
+
+ if (Progress.globalRemainingTime.TotalSeconds > 0 && anyShowRemainingTime && allDefinite)
{
remainingTimeText = $" [{Progress.globalRemainingTime:g}]";
}
+ Progress.Item currentItem;
+
+ // If we only have a single top-level item, show the label of that item instead of "Multiple tasks"
+ if (topLevelRunningCount <= 1 && firstTopLevelItem != null)
+ {
+ taskCount = 1;
+ currentItem = firstTopLevelItem;
+ }
+ else
+ {
+ currentItem = Array.Find(progressItems, item => item.priority != (int)Progress.Priority.Idle);
+ }
+
+ if (currentItem != null && !string.IsNullOrEmpty(currentItem.description))
+ m_ProgressStatus.tooltip = currentItem.name + "\r\n" + currentItem.description;
+ m_ProgressPercentageStatus.text = Progress.globalProgress.ToString("P", percentageFormat);
+
if (taskCount > 1)
m_ProgressStatus.text = $"Multiple tasks ({taskCount}){remainingTimeText}";
else
diff --git a/Editor/Mono/GUI/ColorPicker.cs b/Editor/Mono/GUI/ColorPicker.cs
index fb7ffede3a..8523f76007 100644
--- a/Editor/Mono/GUI/ColorPicker.cs
+++ b/Editor/Mono/GUI/ColorPicker.cs
@@ -3,6 +3,7 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using Unity.Scripting.LifecycleManagement;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Accessibility;
@@ -11,7 +12,7 @@
namespace UnityEditor
{
[VisibleToOtherModules("UnityEditor.GraphToolkitModule")]
- internal class ColorPicker : EditorWindow
+ internal partial class ColorPicker : EditorWindow
{
private const string k_HeightPrefKey = "CPickerHeight";
private const string k_ShowDefaultsPrefKey = "CPDefaultsShow";
@@ -43,6 +44,7 @@ internal class ColorPicker : EditorWindow
const int kColorBoxSize = 32;
[SerializeField]
Texture2D m_ColorBox;
+ [NoAutoStaticsCleanup] // hash code is stable across code reloads
static int s_Slider2Dhash = "Slider2D".GetHashCode();
[SerializeField]
bool m_ShowDefaults = true;
@@ -142,8 +144,10 @@ public static ColorPicker instance
return s_Instance;
}
}
+ [AutoStaticsCleanupOnCodeReload]
static ColorPicker s_Instance;
+ [NoAutoStaticsCleanup] // numeric keyboard control value, safe to persist
public static int originalKeyboardControl { get; private set; }
// ------- Soerens 2D slider --------
@@ -473,6 +477,7 @@ static class Styles
public static readonly float hueDialThumbSize;
+ [NoAutoStaticsCleanup] // immutable padding constants, safe to persist
public static readonly RectOffset colorBoxPadding = new RectOffset(6, 6, 6, 6);
public static readonly Color lowLuminanceContentColor = Color.white;
@@ -1158,7 +1163,9 @@ void HandleCopyPasteEvents()
}
}
+ [NoAutoStaticsCleanup] // HideAndDontSave texture survives reload; re-initialized lazily if null
static Texture2D s_LeftGradientTexture;
+ [NoAutoStaticsCleanup] // HideAndDontSave texture survives reload; re-initialized lazily if null
static Texture2D s_RightGradientTexture;
public static Texture2D GetGradientTextureWithAlpha1To0()
@@ -1351,16 +1358,19 @@ public void OnDestroy()
}
}
- internal class EyeDropper : GUIView
+ internal partial class EyeDropper : GUIView
{
const int kPixelSize = 10;
// Can't be larger right now since OSX Metal surfaces can't be larger than 16384 pixels.
// This needs to be changed to a larger size since it will miss mouse events when using multiple 4K monitors
private const int kDummyWindowSize = 100;
+ [NoAutoStaticsCleanup] // last picked color, value type, safe to persist
internal static Color s_LastPickedColor;
GUIView m_DelegateView;
Texture2D m_Preview;
+ [AutoStaticsCleanupOnCodeReload]
static EyeDropper s_Instance;
+ [NoAutoStaticsCleanup] // value type, safe to persist; overwritten at start of each EyeDropper session
private static Vector2 s_PickCoordinates = Vector2.zero;
private bool m_IsOpened;
private bool m_IsCancelled;
@@ -1388,7 +1398,9 @@ static void Start(GUIView viewToUpdate, Action colorPickedCallback)
win.m_DontSaveToLayout = true;
win.title = "EyeDropper";
win.hideFlags = HideFlags.DontSave;
+#pragma warning disable UAL0018 // assigned to a ScriptableObject field, and as scriptable objects are cleaned up (serialized and recreated) on code reload, this should be good
win.rootView = instance;
+#pragma warning restore UAL0018
win.Show(ShowMode.PopupMenu, loadPosition: true, displayImmediately: true, setFocus: true);
instance.AddToAuxWindowList();
win.SetInvisible();
diff --git a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerIMGUI.cs b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerIMGUI.cs
index 3a77095854..1351d037b5 100644
--- a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerIMGUI.cs
+++ b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerIMGUI.cs
@@ -9,6 +9,7 @@
using UnityEngine.UIElements;
using UnityEditor.IMGUI.Controls;
using UnityEditor.UIElements;
+using Unity.Scripting.LifecycleManagement;
using TreeView = UnityEditor.IMGUI.Controls.TreeView;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem;
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState;
@@ -35,7 +36,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten
/// for UITK so the partial ends up with only the
/// PropertyDrawer overrides delegating into here.
///
- sealed class DrawerInstanceIMGUI
+ sealed partial class DrawerInstanceIMGUI
{
// Per-(property, container) IMGUI state cache. In-memory, editor-process lifetime.
// Key: (propertyPath, targetEntityId, imguiContainerId).
@@ -45,8 +46,15 @@ sealed class DrawerInstanceIMGUI
// availableWidth (see GetOrCreate with isMultiEdit: true). GetOrCreate promotes a
// stub to a full entry on demand while preserving any width already observed
// during a prior short-circuit frame.
+ [AutoStaticsCleanupOnCodeReload]
static readonly Dictionary s_Cache = new();
+ internal static void InvalidateAllSortOrders()
+ {
+ foreach (var instance in s_Cache.Values)
+ instance.InvalidateSortOrder();
+ }
+
static class Styles
{
// k_TreeViewHeight caps the rows area so the IMGUI drawer doesn't grow unbounded
@@ -64,18 +72,23 @@ static class Styles
public const float k_RowVerticalPadding = 5f;
public const float k_CellHorizontalPadding = 8f;
public const float k_ValueLeftPadding = 16f;
+ public const float k_OneColumnValueIndent = 14f;
+ public const float k_StaticValueHeaderTopMargin = 2f;
public const float k_CellLabelWidthFraction = 0.35f;
public const float k_CellLabelMinWidth = 80f;
public const float k_CellControlMinWidth = 40f;
- public const float k_DuplicateKeyIconLeftMargin = 4f;
- public const float k_DuplicateKeyIconTopOffset = 2f;
- public const float k_DuplicateKeyIconSize = 14f;
+ public const float k_KeyWarningIconLeftMargin = 4f;
+ public const float k_KeyWarningIconTopOffset = 2f;
+ public const float k_KeyWarningIconSize = 14f;
public const float k_HandleWidth = 6f;
public const float k_SortArrowSize = 12f;
public const float k_SelectionBorderWidth = 3f;
public const float k_VerticalScrollbarWidth = 16f;
- public const float k_DuplicatesHelpBoxTopMargin = 4f;
- public const float k_DuplicatesHelpBoxBottomMargin = 4f;
+ public const float k_BoxBottomBorder = 1f;
+ public const float k_EmptyRowsAreaPadding = 8f;
+ public const float k_EmptyLabelIndent = 18f;
+ public const float k_IgnoredHelpBoxTopMargin = 4f;
+ public const float k_IgnoredHelpBoxBottomMargin = 4f;
public static readonly GUIStyle headerBackground = "RL Header";
public static readonly GUIStyle boxBackground = "RL Background";
@@ -84,11 +97,11 @@ static class Styles
public static readonly GUIStyle columnLabel = "MultiColumnHeader";
public static readonly GUIStyle columnLabelClipped = new GUIStyle(columnLabel) { clipping = TextClipping.Ellipsis };
- public static GUIContent iconPlus = EditorGUIUtility.TrIconContent("Toolbar Plus");
- public static GUIContent iconMinus = EditorGUIUtility.TrIconContent("Toolbar Minus");
+ public static readonly GUIContent iconPlus = EditorGUIUtility.TrIconContent("Toolbar Plus");
+ public static readonly GUIContent iconMinus = EditorGUIUtility.TrIconContent("Toolbar Minus");
- public static Texture2D sortAscIcon = EditorGUIUtility.LoadIconRequired("UIPackageResources/Images/scrollup_uielements.png");
- public static Texture2D sortDescIcon = EditorGUIUtility.LoadIconRequired("UIPackageResources/Images/scrolldown_uielements.png");
+ public static readonly Texture2D sortAscIcon = EditorGUIUtility.LoadIconRequired("UIPackageResources/Images/scrollup_uielements.png");
+ public static readonly Texture2D sortDescIcon = EditorGUIUtility.LoadIconRequired("UIPackageResources/Images/scrolldown_uielements.png");
}
readonly struct PropertyCacheKey : IEquatable
@@ -126,6 +139,7 @@ public bool Equals(PropertyCacheKey other)
public readonly SerializedProperty arrayProperty;
public SortedIndexMap sortedIndices = SortedIndexMap.Empty;
public readonly HashSet duplicateEntryIndices = new HashSet();
+ public readonly HashSet nullKeyEntryIndices = new HashSet();
// Count of items the TreeView is currently rendering. Equal to
// sortedIndices.Length by invariant; may differ from arrayProperty.arraySize
// between an external array mutation (Undo, script, prefab apply,
@@ -144,6 +158,8 @@ public bool Equals(PropertyCacheKey other)
// with sortedIndices.
public ulong lastKnownKeysHash;
public bool needsReload;
+ public bool needsSortOrderRebuild;
+ public bool needsLayoutChange;
public bool needsDuplicate;
public readonly Type keyType;
public readonly Type valueType;
@@ -154,10 +170,32 @@ public bool Equals(PropertyCacheKey other)
public bool hasStaticInlineHeight; // inline compound type with fixed height (no expandable children)
public bool needsHeightClassification; // deferred until first element exists to inspect
public bool needsHeightRefresh; // a rendered row's measured height drifted from cached value
+ public bool needsHeightMeasure; // row-height measurement deferred to the next OnGUI Layout pass
public bool sortAscending = true;
+ // Single source of truth for the column layout; oneColumnMode/useValueFoldouts are
+ // derived views kept so the row-drawing code reads intent-named flags. The two
+ // OneColumn_* modes both stack key over value, differing only in the per-row foldout.
+ public DictionaryLayout layout = DictionaryLayout.TwoColumns;
+ public bool oneColumnMode => layout != DictionaryLayout.TwoColumns;
+ public bool useValueFoldouts => layout == DictionaryLayout.OneColumnWithValueFoldout;
+ // Default layout resolved from [DictionaryDisplay] (field- or assembly-level); the
+ // active layout falls back to this until the user overrides it from the context menu.
+ public readonly DictionaryLayout attributeLayout;
+ public readonly GUIContent valueLabelContent;
+ // Value-cell label when the value type is a collection ("Dictionary"/"Array"/"List"); null
+ // otherwise, so the cell draws with GUIContent.none. This is what gives a nested collection value
+ // its foldout title: it is forwarded as the label to the value's own drawer (a nested dictionary
+ // reads it as its OnGUI label, an array/list as its built-in foldout title).
+ public readonly GUIContent valueCollectionLabel;
public readonly float attributeKeyFraction;
public readonly Hash128 stateCacheKey;
public float availableWidth;
+ // Room the host has below the drawer. 0 means something outside can scroll and reveal the
+ // rows by itself, or that no Repaint has measured yet.
+ public float hostRoomForRows;
+
+ DictionaryState m_CachedViewState;
+ int m_CachedStateVersion = -1;
// Stubs allocated by GetOrCreate(..., isMultiEdit: true) leave treeView null;
// a null treeView is the stable marker that distinguishes a stub from a fully-
@@ -187,10 +225,17 @@ public bool Equals(PropertyCacheKey other)
// PerformSortToggle doesn't depend on selection that may have moved by
// the time it runs.
public bool deferredWorkScheduled;
- public bool needsDuplicateRefresh;
+ public bool needsMarkerRefresh;
public bool pendingSortToggle;
public int[] pendingSortToggleSelectionArrayIndices;
public bool needsTreeViewFocus;
+ // Display index to (re)frame once row heights have settled. A structural mutation
+ // (AddEntry) runs before the deferred height measurement is consumed on the Layout
+ // pass, so framing right away scrolls against pre-measurement row rects and can miss
+ // the target — e.g. a freshly added bottom row not scrolled fully into view. We record
+ // the target here and frame it from GetExpandedPropertyHeight once heights are settled.
+ // -1 means nothing is pending.
+ public int pendingFrameDisplayIndex = -1;
// Interaction check. When RunDeferredStructuralWork finds needsReload but
// EditorInteractionMonitor.IsReadyToApplyDeferredChanges is false, we install a
@@ -232,22 +277,32 @@ public bool Equals(PropertyCacheKey other)
dictionaryProperty = property.Copy();
- var genericArgs = GetDictionaryGenericArguments(m_FieldInfo);
+ // The property's static type is the closed Dictionary for *this* field/value, which
+ // for a nested dictionary differs from m_FieldInfo.FieldType (the outer field). Prefer it
+ // for the key/value types and layout lookup; fall back to the field type if unavailable.
+ ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromProperty(property, out var dictionaryType);
+ var genericArgs = dictionaryType != null && dictionaryType.IsGenericType
+ ? dictionaryType.GetGenericArguments()
+ : GetDictionaryGenericArguments(m_FieldInfo);
keyType = genericArgs[0];
valueType = genericArgs[1];
keyHasCustomDrawer = ScriptAttributeUtility.GetDrawerTypeForType(keyType, null) != null;
valueHasCustomDrawer = ScriptAttributeUtility.GetDrawerTypeForType(valueType, null) != null;
- GetHeaderLabels(m_FieldInfo, out var keyLabel, out var valueLabel, out var keyFraction);
+ GetHeaderLabels(m_FieldInfo, dictionaryType, out var keyLabel, out var valueLabel, out var keyFraction);
attributeKeyFraction = keyFraction;
+ attributeLayout = ResolveDefaultLayout(m_FieldInfo, dictionaryType);
+ valueLabelContent = new GUIContent(valueLabel);
+ var collectionLabel = GetNestedCollectionValueLabel(valueType);
+ if (collectionLabel != null)
+ valueCollectionLabel = new GUIContent(collectionLabel);
stateCacheKey = ComputeStateCacheKey(property.propertyPath);
float effectiveFraction = GetActiveKeyColumnFraction(stateCacheKey, keyFraction);
+ layout = GetActiveLayout(stateCacheKey, attributeLayout);
var cachedState = s_StateCache.GetState(stateCacheKey);
if (cachedState != null)
- {
sortAscending = cachedState.sortAscending;
- }
header = new DictionaryHeader(keyLabel, valueLabel, effectiveFraction, stateCacheKey);
treeViewState = new TreeViewState();
@@ -255,15 +310,11 @@ public bool Equals(PropertyCacheKey other)
arrayProperty = GetArrayProperty(dictionaryProperty);
sortedIndices = SortedIndexMap.Build(arrayProperty, sortAscending);
lastKnownKeysHash = GetKeysContentHash(arrayProperty);
- TryRefreshDuplicateIndicesInto(dictionaryProperty, duplicateEntryIndices);
+ TryRefreshDuplicateAndNullKeyIndicesInto(dictionaryProperty, duplicateEntryIndices, nullKeyEntryIndices);
treeView = new DictionaryTreeView(this);
- if (displayedItemCount > 0)
- ClassifyRowHeights();
- else
- needsHeightClassification = IsGenericInlineType(keyType, keyHasCustomDrawer)
- || IsGenericInlineType(valueType, valueHasCustomDrawer);
+ ClassifyRowHeights();
treeView.Reload();
}
@@ -300,7 +351,7 @@ public static DrawerInstanceIMGUI GetOrCreate(DictionaryDrawer drawer, Serialize
RegisterCacheEvictionOnDetach(imguiContainer, key);
// Bind a property-change listener on the IMGUIContainer so any inspector
- // showing this dictionary re-sorts and refreshes its duplicate markers when
+ // showing this dictionary re-sorts and refreshes its key warning markers when
// the SerializedObject is mutated elsewhere (e.g. a key edit in a second
// inspector pinned to the same target). Stubs (multi-edit) have no
// dictionaryProperty to track, so we only register on fully-initialized
@@ -322,7 +373,11 @@ public static float GetPropertyHeight(DictionaryDrawer drawer, SerializedPropert
if (!property.isExpanded)
return EditorGUIUtility.singleLineHeight;
- // Invariant: PropertyDrawer's OnGUI / GetPropertyHeight only run while in an IMGUIContainer
+ // Invariant: size is only ever computed inside an OnGUI pass, so an IMGUIContainer
+ // is always on the stack here. The deferred reload (delayCall / update) rebuilds row
+ // *structure* but never measures, so it can't reach this from a container-less
+ // context — including a nested dictionary's GetPropertyHeight queried via the parent
+ // drawer's row measurement, which now runs on the parent's OnGUI Layout pass.
var imguiContainer = IMGUIContainer.GetCurrentIMGUIContainer();
Debug.Assert(imguiContainer != null, Texts.ExpectedCurrentContainerMessage);
@@ -360,43 +415,128 @@ public bool HasFocus()
float GetExpandedPropertyHeight()
{
- if (dynamicRowHeight && displayedItemCount > 0 && needsHeightRefresh)
- {
- treeView.RefreshCustomRowHeights();
- needsHeightRefresh = false;
- }
+ // Row heights are measured only on the OnGUI Layout pass, where an IMGUIContainer is
+ // guaranteed (the inspector calls GetPropertyHeight from its own OnGUI). The deferred
+ // reload rebuilds row *structure* and classifies cells, but never measures: measuring
+ // a custom-drawer cell (e.g. a nested dictionary) calls into that drawer's
+ // GetPropertyHeight, which needs the container. We measure here and let
+ // RefreshCustomRowHeights cache it so the Repaint / event passes of the same frame
+ // read a consistent total.
+ //
+ // For variable-height rows we measure *every* row, not just a sample: with custom row
+ // heights the TreeView's scroll view clamps scrollPos to (totalHeight - viewport)
+ // each frame, so a totalHeight that keeps changing as rows are lazily measured would
+ // repeatedly clamp the scroll and walk it away from a framed position. Measuring all
+ // rows once makes totalHeight exact and stable, so framing (and the scrollbar) hold.
+ MeasureRowHeightsIfNeeded();
+
+ ApplyPendingFrameIfReady();
+
+ return GetHeightAroundRows() + GetRowsAreaHeight();
+ }
- float foldoutLine = EditorGUIUtility.singleLineHeight;
- float columnHeader = header.height;
- float rowsArea;
+ float GetHeightAroundRows()
+ {
+ // The help box is reserved with its margins; HasIgnoredHelpBox reports the box
+ // alone, since that is what DrawIgnoredHelpBox needs for the rect it draws into.
+ float heightOfHelpboxAndMargins = 0f;
+ if (HasIgnoredHelpBox(out float helpboxHeight, out _))
+ heightOfHelpboxAndMargins = Styles.k_IgnoredHelpBoxTopMargin + helpboxHeight + Styles.k_IgnoredHelpBoxBottomMargin;
+
+ return EditorGUIUtility.singleLineHeight
+ + header.height
+ + Styles.k_BoxBottomBorder
+ + Styles.k_FooterHeight + Styles.k_FooterSpacing
+ + heightOfHelpboxAndMargins;
+ }
+ float GetRowsAreaHeight()
+ {
if (displayedItemCount == 0)
+ return EditorGUIUtility.singleLineHeight + Styles.k_EmptyRowsAreaPadding;
+
+ return Mathf.Min(treeView.totalHeight, GetMaxRowsAreaHeight());
+ }
+
+ // GUI.BeginScrollView takes its scroll range from the rect it is given, so in a host that
+ // cannot scroll, rows below the part of that rect it can show would be unreachable. (UUM-149490)
+ float GetMaxRowsAreaHeight()
+ {
+ if (hostRoomForRows <= 0f)
+ return Styles.k_TreeViewHeight;
+
+ return Mathf.Clamp(hostRoomForRows - GetHeightAroundRows(),
+ EditorGUIUtility.singleLineHeight, Styles.k_TreeViewHeight);
+ }
+
+ // An enclosing IMGUI scroll view counts too: a nested dictionary sits inside the outer
+ // list's, and clamping it there would tie its height to the outer scroll position.
+ bool HostCanScroll()
+ => GUI.GetTopScrollView() != null
+ || imguiContainer?.GetFirstAncestorOfType() != null;
+
+ void MeasureRowHeightsIfNeeded()
+ {
+ if (Event.current.type == EventType.Layout && displayedItemCount > 0 && needsHeightMeasure)
{
- rowsArea = EditorGUIUtility.singleLineHeight + 8f;
+ needsHeightMeasure = false;
+ if (variableRowHeight)
+ treeView.MeasureAllRowHeights();
+ else if (hasStaticInlineHeight)
+ treeView.ComputeFixedInlineRowHeight();
+ treeView.RefreshCustomRowHeights();
}
- else
+
+ if ((dynamicRowHeight || hasStaticInlineHeight) && displayedItemCount > 0 && needsHeightRefresh)
{
- rowsArea = Mathf.Min(treeView.totalHeight, Styles.k_TreeViewHeight);
+ treeView.RefreshCustomRowHeights();
+ needsHeightRefresh = false;
}
+ }
+
+ // Apply a deferred frame request (e.g. a newly added row) once row heights are settled —
+ // after MeasureRowHeightsIfNeeded has run on this Layout pass — so the scroll uses final
+ // row rects instead of the pre-measurement estimate. Repaint so the scroll position the
+ // frame sets is rendered.
+ void ApplyPendingFrameIfReady()
+ {
+ if (pendingFrameDisplayIndex < 0 || Event.current.type != EventType.Layout || needsHeightMeasure || needsHeightRefresh)
+ return;
- float footer = Styles.k_FooterHeight + Styles.k_FooterSpacing;
- float duplicatesBlock = CalcDuplicatesHelpBoxHeight(availableWidth);
- return foldoutLine + columnHeader + rowsArea + 1f + footer + duplicatesBlock;
+ if (pendingFrameDisplayIndex < displayedItemCount)
+ {
+ treeView.FrameItem(pendingFrameDisplayIndex);
+ HandleUtility.Repaint();
+ }
+ pendingFrameDisplayIndex = -1;
}
- float CalcDuplicatesHelpBoxHeight(float availWidth)
+ // True when the ignored-entries help box is rendered: something to report, and the content
+ // width is known (it is sampled from the first Repaint — see UpdateAvailableWidth). The
+ // height and the draw both go through here, so a block can't be drawn without being
+ // reserved, which would land it outside the drawer's rect and over the rest of the inspector.
+ bool HasIgnoredHelpBox(out float height, out string text)
{
- if (duplicateEntryIndices.Count == 0 || availWidth <= 0f)
- return 0f;
+ height = 0f;
+ text = null;
+ int duplicateCount = duplicateEntryIndices.Count;
+ int nullKeyCount = nullKeyEntryIndices.Count;
+ if (duplicateCount + nullKeyCount == 0 || availableWidth <= 0f)
+ return false;
- string text = Texts.GetDuplicatesHelpBoxText(duplicateEntryIndices.Count);
- float helpBoxHeight = DrawerEditorGUI.GetHelpBoxWithButtonHeight(MessageType.Warning, text, availWidth);
- return Styles.k_DuplicatesHelpBoxTopMargin + helpBoxHeight + Styles.k_DuplicatesHelpBoxBottomMargin;
+ text = Texts.GetIgnoredHelpBoxText(duplicateCount, nullKeyCount);
+ height = DrawerEditorGUI.GetHelpBoxWithButtonHeight(MessageType.Warning, text, availableWidth);
+ return true;
}
void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool isMultiEdit)
{
UpdateAvailableWidth(position);
+ RepaintIfNeeded();
+
+ // Only Repaint carries the drawer's final rect; a Layout pass hands out a dummy one.
+ if (Event.current.type == EventType.Repaint)
+ hostRoomForRows = HostCanScroll() ? 0f : GUIClip.visibleRect.yMax - position.y;
var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
@@ -419,6 +559,8 @@ void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool is
return;
}
+ SyncWithDictionaryViewState();
+
// Pick up external array-size changes (Undo, script, prefab apply, etc.)
// before drawing so the row-count text in the foldout reflects them this
// frame. The actual rebuild is deferred to PerformReload between OnGUI
@@ -446,6 +588,8 @@ void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool is
return;
}
+ MeasureRowHeightsIfNeeded();
+
// Expanded body: Two column header, rows (or empty label), footer and help box
DrawExpandedBody(position, foldoutRect.yMax, property);
}
@@ -525,12 +669,21 @@ bool IsAlive()
void ClearAllPendingFlags()
{
needsReload = false;
- needsDuplicateRefresh = false;
+ needsSortOrderRebuild = false;
+ needsLayoutChange = false;
+ needsMarkerRefresh = false;
pendingSortToggle = false;
pendingSortToggleSelectionArrayIndices = null;
StopInteractionCheck();
}
+ public void InvalidateSortOrder()
+ {
+ needsReload = true;
+ needsSortOrderRebuild = true;
+ ScheduleDeferredStructuralWork();
+ }
+
// Detect external array-size changes and (re-)arm the deferred reload.
// needsReload may already be set by a TrackPropertyValue notification; the
// OR keeps that pending request scheduled even on a frame where the size
@@ -545,6 +698,48 @@ void ScheduleReloadIfArrayChanged()
ScheduleDeferredStructuralWork();
}
+ // Sibling dictionaries (elements sharing a normalized stateCacheKey) share one persisted
+ // DictionaryState. UITK links live views and pushes; IMGUI is immediate-mode, so each
+ // instance instead pulls the shared state each frame and applies any divergence. Structural
+ // changes (sort/layout) are deferred like every other reload; the column fraction is a pure
+ // draw-time value, so it's applied inline. StateCache hands back the same DictionaryState
+ // instance to every sibling, so a resize drag mutating that object in-memory (see
+ // HandleResize) is picked up here without a per-frame disk write.
+ void SyncWithDictionaryViewState()
+ {
+ // Only re-read when shared state changed; a default dictionary would otherwise run GetState -> File.Exists every event.
+ if (m_CachedStateVersion != StateVersion)
+ {
+ m_CachedViewState = GetCachedState(stateCacheKey);
+ m_CachedStateVersion = StateVersion;
+ }
+ var state = m_CachedViewState;
+
+ bool cachedSortAscending = state?.sortAscending ?? true;
+ if (cachedSortAscending != sortAscending && !pendingSortToggle)
+ {
+ sortAscending = cachedSortAscending;
+ needsReload = true;
+ needsSortOrderRebuild = true;
+ ScheduleDeferredStructuralWork();
+ }
+
+ var effectiveLayout = GetActiveLayout(state, attributeLayout);
+ if (effectiveLayout != layout)
+ {
+ layout = effectiveLayout;
+ needsLayoutChange = true;
+ ScheduleDeferredStructuralWork();
+ }
+
+ float effectiveFraction = GetActiveKeyColumnFraction(state, attributeKeyFraction);
+ if (!Mathf.Approximately(header.column1Fraction, effectiveFraction))
+ {
+ header.column1Fraction = effectiveFraction;
+ imguiContainer?.MarkDirtyRepaint();
+ }
+ }
+
// Coalescing entry point for every structural mutation. Caller flips a pending flag
// (or fills a snapshot) on the instance and then calls this; we register at most one
// EditorApplication.delayCall per instance per "burst", regardless of how many flags
@@ -558,6 +753,33 @@ void ScheduleDeferredStructuralWork()
EditorApplication.delayCall += RunDeferredStructuralWork;
}
+ // Set by RepaintForHeightChange, consumed on the next Repaint pass.
+ bool m_NeedsFollowUpRepaint;
+
+ // Use instead of MarkDirtyRepaint when the height from GetPropertyHeight changes with no
+ // user event left to settle the layout (deferred structural work, context-menu actions,
+ // first width sample). Two repaints are needed: whoever lays us out from a cached height
+ // — an enclosing ReorderableList, the IMGUIContainer's measured layout — only notices
+ // while repainting and drops the stale value after drawing that frame, so the first
+ // repaint lands against the old geometry and the second comes out right. Same reason
+ // moving the mouse over the Inspector fixes it.
+ void RepaintForHeightChange()
+ {
+ m_NeedsFollowUpRepaint = true;
+ imguiContainer?.MarkDirtyRepaint();
+ }
+
+ // Chained from a Repaint pass rather than requested up front, so it cannot be coalesced
+ // into the repaint RepaintForHeightChange already asked for.
+ void RepaintIfNeeded()
+ {
+ if (!m_NeedsFollowUpRepaint || Event.current.type != EventType.Repaint)
+ return;
+
+ m_NeedsFollowUpRepaint = false;
+ HandleUtility.Repaint();
+ }
+
// Installs a single EditorApplication.update handler that re-checks the
// EditorInteractionMonitor gate every k_InteractionCheckIntervalSeconds. Only one
// handler is registered per instance at a time; subsequent calls are no-ops while
@@ -611,7 +833,7 @@ void RunInteractionCheck()
// Runs strictly between OnGUI passes. Order matters: sort toggle runs first
// because it rebuilds sortedIndices wholesale, which makes a subsequent gated
// reload either a no-op or correctly idempotent; needsReload then
- // needsDuplicateRefresh follow in decreasing structural impact. The
+ // needsMarkerRefresh follow in decreasing structural impact. The
// interaction gate only applies to needsReload — SortToggle originates from
// an explicit user click that is itself the interaction, so re-arming would
// just spin.
@@ -629,6 +851,14 @@ void RunDeferredStructuralWork()
bool needsRepaint = false;
+ if (needsLayoutChange)
+ {
+ needsLayoutChange = false;
+ ClassifyRowHeights();
+ treeView.Reload();
+ needsRepaint = true;
+ }
+
if (pendingSortToggle)
{
PerformSortToggle();
@@ -641,23 +871,24 @@ void RunDeferredStructuralWork()
{
int currentSize = arrayProperty.arraySize;
bool sizeChanged = currentSize != displayedItemCount;
- bool keysChanged = sizeChanged || GetKeysContentHash(arrayProperty) != lastKnownKeysHash;
+ bool keysChanged = sizeChanged || needsSortOrderRebuild || GetKeysContentHash(arrayProperty) != lastKnownKeysHash;
if (!keysChanged)
{
// Pure value-only edit. Duplicates are determined solely by key
// content, so a same-hash refresh would also be a no-op.
needsReload = false;
- needsDuplicateRefresh = false;
+ needsMarkerRefresh = false;
StopInteractionCheck();
}
else if (EditorInteractionMonitor.IsReadyToApplyDeferredChanges(null))
{
PerformReload();
needsReload = false;
- // A full reload also recomputes duplicateEntryIndices, so a pending
- // duplicate-only refresh is subsumed and can be cleared.
- needsDuplicateRefresh = false;
+ // A full reload also recomputes both marker sets, so a pending
+ // marker-only refresh is subsumed and can be cleared.
+ needsMarkerRefresh = false;
+ needsSortOrderRebuild = false;
needsRepaint = true;
StopInteractionCheck();
}
@@ -666,24 +897,26 @@ void RunDeferredStructuralWork()
// Interaction is in flight (text edit, hot control, picker open) so
// start a EditorApplication.update handler that checks when
// the user is done editing at a coarse interval and re-enters
- // ScheduleDeferredStructuralWork once the gate opens. The duplicate
- // refresh below still runs ungated so the per-row duplicate-key
- // icons and the "X duplicates" count keep updating live as the
+ // ScheduleDeferredStructuralWork once the gate opens. The marker
+ // refresh below still runs ungated so the per-row key warning
+ // icons and the "X ignored" count keep updating live as the
// user types.
- needsDuplicateRefresh = true;
+ needsMarkerRefresh = true;
StartInteractionCheck();
}
}
- if (needsDuplicateRefresh)
+ if (needsMarkerRefresh)
{
- needsDuplicateRefresh = false;
- if (TryRefreshDuplicateIndicesInto(dictionaryProperty, duplicateEntryIndices))
+ needsMarkerRefresh = false;
+ if (TryRefreshDuplicateAndNullKeyIndicesInto(dictionaryProperty, duplicateEntryIndices, nullKeyEntryIndices))
needsRepaint = true;
}
+ // Every branch above changes our height: the row set (reload / sort / layout) or the
+ // ignored-entries help box (marker refresh), so this needs a re-layout, not a repaint.
if (needsRepaint)
- imguiContainer?.MarkDirtyRepaint();
+ RepaintForHeightChange();
}
static bool IsGenericInlineType(Type type, bool hasCustomDrawer)
@@ -695,6 +928,38 @@ static bool IsGenericInlineType(Type type, bool hasCustomDrawer)
return true;
}
+ // True for a key/value type whose cell can render taller than a single line, so the
+ // row height must be derived from the property instead of using the default
+ // single-line height. Covers two cases the row classifier must treat alike:
+ // - generic inline compounds (struct/class drawn by expanding their children), and
+ // - types with a custom PropertyDrawer, which can be multi-line and/or expandable
+ // (e.g. a nested Dictionary<,>). The bare IsGenericInlineType check excludes the
+ // latter, which is why a nested dictionary value otherwise collapses to a single
+ // row line and the nested drawer overlaps the rows below it.
+ // Simple single-line types (primitive, string, enum, Object reference) return false.
+ static bool IsComplexCellType(Type type, bool hasCustomDrawer)
+ {
+ if (type == null)
+ return false;
+ return hasCustomDrawer || IsGenericInlineType(type, false);
+ }
+
+ // Whether a cell's rendered height can change after the initial layout, which forces
+ // per-row lazy height tracking instead of a single fixed row height. A custom-drawer cell
+ // is always treated as dynamic because the drawer can expand (foldout) or resize at
+ // runtime (e.g. a nested Dictionary<,>) and its height at classification time — while
+ // collapsed — is not representative. An array/list cell is likewise dynamic: it has a
+ // foldout and a resizable element count. A generic inline compound is dynamic only when
+ // it contains expandable children.
+ static bool CellHeightCanChange(SerializedProperty prop, bool hasCustomDrawer)
+ {
+ if (hasCustomDrawer)
+ return true;
+ if (prop != null && prop.isArray)
+ return true;
+ return HasExpandableChildren(prop);
+ }
+
static bool HasExpandableChildren(SerializedProperty prop)
{
if (prop == null || !prop.isValid || prop.propertyType != SerializedPropertyType.Generic)
@@ -723,23 +988,57 @@ void ClassifyRowHeights()
{
needsHeightClassification = false;
- bool keyIsInline = IsGenericInlineType(keyType, keyHasCustomDrawer);
- bool valueIsInline = IsGenericInlineType(valueType, valueHasCustomDrawer);
+ dynamicRowHeight = false;
+ variableRowHeight = false;
+ hasStaticInlineHeight = false;
+
+ // OneColumnWithValueFoldout: the per-row foldout is toggled at runtime, which changes
+ // the row height each time it expands/collapses, so these rows are always dynamic
+ // regardless of the key/value types — no element needs to be inspected.
+ if (useValueFoldouts)
+ {
+ dynamicRowHeight = true;
+ variableRowHeight = true;
+ needsHeightMeasure = true;
+ return;
+ }
+
+ bool keyIsComplex = IsComplexCellType(keyType, keyHasCustomDrawer);
+ bool valueIsComplex = IsComplexCellType(valueType, valueHasCustomDrawer);
- if (!keyIsInline && !valueIsInline)
+ // Simple key and value: every row is the same fixed height. Flag it static so
+ // ComputeFixedInlineRowHeight recomputes the shared rowHeight on each switch,
+ // keeping it correct for the current layout.
+ if (!keyIsComplex && !valueIsComplex)
+ {
+ hasStaticInlineHeight = true;
+ needsHeightMeasure = true;
+ return;
+ }
+
+ // A complex cell's height depends on the actual element, which can only be inspected
+ // once one exists. Callers classify eagerly — including on an empty dictionary, so the
+ // layout is ready before the first add — so defer the per-element analysis until the
+ // first entry exists.
+ if (displayedItemCount == 0)
+ {
+ needsHeightClassification = true;
return;
+ }
var element = arrayProperty.GetArrayElementAtIndex(0);
GetKeyAndValueProperties(element, out var keyProp, out var valueProp);
- bool keyDynamic = keyIsInline && HasExpandableChildren(keyProp);
- bool valueDynamic = valueIsInline && HasExpandableChildren(valueProp);
+ bool keyDynamic = keyIsComplex && CellHeightCanChange(keyProp, keyHasCustomDrawer);
+ bool valueDynamic = valueIsComplex && CellHeightCanChange(valueProp, valueHasCustomDrawer);
dynamicRowHeight = keyDynamic || valueDynamic;
variableRowHeight = dynamicRowHeight;
hasStaticInlineHeight = !dynamicRowHeight;
- if (hasStaticInlineHeight)
- treeView.ComputeFixedInlineRowHeight();
+ // Classification only decides the row-height *kind*. The actual measurement
+ // (fixed inline height or variable estimate) runs on the next OnGUI Layout pass —
+ // see GetExpandedPropertyHeight — so this stays safe to call from the deferred reload.
+ needsHeightMeasure = true;
}
// Calculates the rendered height of an EditorGUI.HelpBox(rect, message, type) at a given width
@@ -765,10 +1064,10 @@ void UpdateAvailableWidth(Rect position)
bool wasUnknown = availableWidth <= 0f;
availableWidth = position.width;
- // First valid width sample: repaint so GetPropertyHeight can reserve space
- // on the next frame using the now-known width.
+ // First valid width sample: the width-dependent blocks (multi-edit / ignored-entries
+ // help boxes) reserve and draw nothing until now, so re-layout to pick them up.
if (wasUnknown)
- HandleUtility.Repaint();
+ RepaintForHeightChange();
}
// Multi-edit fallback: the dictionary drawer can't merge two TreeViews / sort
@@ -790,19 +1089,15 @@ void DrawMultiEditHelpBoxIfExpanded(Rect position, Rect foldoutRect, SerializedP
void DrawExpandedBody(Rect position, float startY, SerializedProperty property)
{
float headerH = header.height;
- float fullContentH = displayedItemCount == 0
- ? EditorGUIUtility.singleLineHeight + 8f
- : treeView.totalHeight;
- float contentH = Mathf.Min(fullContentH, Styles.k_TreeViewHeight);
+ float contentH = GetRowsAreaHeight();
- const float borderBottom = 1f;
float y = startY;
// Backgrounds first so the column header / rows draw on top.
if (Event.current.type == EventType.Repaint)
{
var headerRect = new Rect(position.x, y, position.width, headerH);
- var contentRect = new Rect(position.x, y + headerH, position.width, contentH + borderBottom);
+ var contentRect = new Rect(position.x, y + headerH, position.width, contentH + Styles.k_BoxBottomBorder);
Styles.headerBackground.Draw(headerRect, false, false, false, false);
Styles.boxBackground.Draw(contentRect, false, false, false, false);
}
@@ -817,7 +1112,7 @@ void DrawExpandedBody(Rect position, float startY, SerializedProperty property)
// Rows (or "empty dictionary" placeholder when there are no entries).
if (displayedItemCount == 0)
{
- var emptyRect = new Rect(position.x + 18f, y, position.width - 18f, EditorGUIUtility.singleLineHeight + 8f);
+ var emptyRect = new Rect(position.x + Styles.k_EmptyLabelIndent, y, position.width - Styles.k_EmptyLabelIndent, contentH);
EditorGUI.LabelField(emptyRect, Texts.EmptyDictionaryLabel);
y += emptyRect.height;
}
@@ -830,7 +1125,7 @@ void DrawExpandedBody(Rect position, float startY, SerializedProperty property)
treeView.OnGUI(treeRect);
y += contentH;
}
- y += borderBottom;
+ y += Styles.k_BoxBottomBorder;
// Cmd+D / context-menu duplicate is queued during the TreeView OnGUI and
// flushed here, after the rows have already drawn for this frame so we
@@ -845,29 +1140,25 @@ void DrawExpandedBody(Rect position, float startY, SerializedProperty property)
var footerRect = new Rect(position.x, y + Styles.k_FooterSpacing - 1f, position.width, Styles.k_FooterHeight);
DrawFooter(footerRect, property);
- DrawDuplicatesHelpBox(position, footerRect.yMax);
+ if (HasIgnoredHelpBox(out float helpBoxHeight, out var helpBoxText))
+ DrawIgnoredHelpBox(position, footerRect.yMax, helpBoxHeight, helpBoxText);
}
- void DrawDuplicatesHelpBox(Rect position, float startY)
+ void DrawIgnoredHelpBox(Rect position, float startY, float helpBoxHeight, string helpBoxText)
{
- if (duplicateEntryIndices.Count == 0)
- return;
-
- string text = Texts.GetDuplicatesHelpBoxText(duplicateEntryIndices.Count);
- float helpBoxHeight = DrawerEditorGUI.GetHelpBoxWithButtonHeight(MessageType.Warning, text, position.width);
- float helpBoxY = startY + Styles.k_DuplicatesHelpBoxTopMargin;
+ float helpBoxY = startY + Styles.k_IgnoredHelpBoxTopMargin;
var helpBoxRect = new Rect(position.x, helpBoxY, position.width, helpBoxHeight);
- if (DrawerEditorGUI.HelpBoxWithButton(helpBoxRect, MessageType.Warning, text, Texts.SelectFirstDuplicateButtonLabel))
- SelectFirstDuplicate();
+ if (DrawerEditorGUI.HelpBoxWithButton(helpBoxRect, MessageType.Warning, helpBoxText, Texts.SelectFirstIgnoredButtonLabel))
+ SelectFirstIgnored();
}
- void SelectFirstDuplicate()
+ void SelectFirstIgnored()
{
if (treeView == null)
return;
- int firstDisplayIndex = FindFirstDuplicateDisplayIndex(duplicateEntryIndices, sortedIndices);
+ int firstDisplayIndex = FindFirstIgnoredDisplayIndex(duplicateEntryIndices, nullKeyEntryIndices, sortedIndices);
if (firstDisplayIndex < 0)
return;
@@ -878,13 +1169,21 @@ void SelectFirstDuplicate()
void DrawFoldoutHeader(Rect rect, SerializedProperty property, GUIContent label)
{
int duplicateCount = duplicateEntryIndices?.Count ?? 0;
+ int nullKeyCount = nullKeyEntryIndices?.Count ?? 0;
+ int ignoredCount = duplicateCount + nullKeyCount;
int itemCount = displayedItemCount;
- string countText = Texts.GetItemCountText(itemCount);
- string duplicateText = duplicateCount > 0
- ? Texts.GetDuplicateCountText(duplicateCount)
- : "";
- string infoText = $"{countText}{duplicateText}";
+ string infoText;
+ if (DictionaryDrawer.ShowSerializedOrder)
+ {
+ infoText = Texts.ShowingSerializedOrderInfoLabel;
+ }
+ else
+ {
+ infoText = Texts.GetItemCountText(itemCount);
+ if (ignoredCount > 0)
+ infoText += Texts.GetIgnoredCountText(ignoredCount);
+ }
var infoSize = EditorStyles.miniLabel.CalcSize(new GUIContent(infoText));
var infoRect = new Rect(rect.xMax - infoSize.x - 4f, rect.y, infoSize.x, rect.height);
@@ -911,7 +1210,7 @@ void DrawFooter(Rect rect, SerializedProperty property)
}
var selection = treeView.GetSelection();
- using (new EditorGUI.DisabledScope(selection.Count == 0))
+ using (new EditorGUI.DisabledScope(arrayProperty.arraySize == 0))
{
if (GUI.Button(removeRect, Styles.iconMinus, Styles.footerButton))
{
@@ -922,25 +1221,23 @@ void DrawFooter(Rect rect, SerializedProperty property)
void SetTreeViewFocusOnMouseEvents(Rect treeRect)
{
- // TreeView focus grab on MouseDown / ScrollWheel. Must be called before the
- // TreeView's OnGUI().
- // Two reasons stack:
- // - Visual: the active (blue) selection outline only shows when the
- // treeview itself owns keyboard focus, mirroring how row clicks
- // already grab focus via HandleRowSelectionClick.
- // - Correctness: scrolling (wheel, scrollbar drag, repeat-button) culls
- // rows that leave the visible area, and culled controls don't allocate
- // their controlIDs — that shifts the IDs of the rows that remain and
- // reroutes keyboard focus to an unrelated cell. Releasing whatever
- // currently owns keyboard focus (typically a text field in a row)
- // before the scroll runs avoids that. SetFocus also clears
- // EditorGUIUtility.editingTextField, so a text edit ends cleanly.
+ // TreeView focus grab on ScrollWheel. Must be called before the TreeView's OnGUI().
+ //
+ // This ends an in-progress cell text edit when the user wheel-scrolls the list and
+ // hands focus (and the blue selection outline) to the treeview itself: SetFocus moves
+ // keyboardControl to the treeview and clears EditorGUIUtility.editingTextField.
+ //
+ // Note it is intentionally NOT done for plain MouseDown. Scrolling culls rows that
+ // leave the visible area, but the cells' control ids are position-keyed and stay
+ // stable across culling, so a focused cell is never rerouted to a different row —
+ // grabbing focus on MouseDown would only steal it from the cell the user just clicked,
+ // breaking caret placement (first click selects all, second could never place the caret).
//
// OnOptimizedInspectorGUI(Rect contentRect) clears GUIUtility.keyboardControl
// = 0 even when we have treeview focus, so the !HasFocus() check is needed
// here too — without it SetFocus would no-op when the user is just panning
// over the treeview and we want the blue outline back.
- if ((Event.current.type == EventType.MouseDown || Event.current.type == EventType.ScrollWheel)
+ if (Event.current.type == EventType.ScrollWheel
&& treeRect.Contains(Event.current.mousePosition)
&& !treeView.HasFocus())
treeView.SetFocus();
@@ -978,13 +1275,12 @@ void AddEntry()
sortedIndices = SortedIndexMap.Build(arrayProperty, sortAscending);
lastKnownKeysHash = GetKeysContentHash(arrayProperty);
- TryRefreshDuplicateIndicesInto(dictionaryProperty, duplicateEntryIndices);
+ TryRefreshDuplicateAndNullKeyIndicesInto(dictionaryProperty, duplicateEntryIndices, nullKeyEntryIndices);
if (needsHeightClassification)
ClassifyRowHeights();
treeView.Reload();
- int newDisplayIndex = sortedIndices.ToDisplayIndex(lastIndex);
- treeView.SetSelection(new[] { newDisplayIndex }, TreeViewSelectionOptions.RevealAndFrame);
+ SelectAndFrameAfterRebuild(new[] { sortedIndices.ToDisplayIndex(lastIndex) });
treeView.SetFocus();
}
@@ -993,14 +1289,15 @@ void RemoveSelectedEntries()
var selection = treeView.GetSelection();
int newSelectedDisplayIndex = selection.Count == 1 ? selection[0] : -1;
- bool removed = RemoveEntriesAtDisplayIndices(
- arrayProperty, selection, sortedIndices);
+ var removed = selection.Count > 0
+ ? RemoveEntriesAtDisplayIndices(arrayProperty, selection, sortedIndices)
+ : RemoveEntryAtDisplayIndex(arrayProperty, arrayProperty.arraySize - 1, sortedIndices);
if (!removed)
return;
sortedIndices = SortedIndexMap.Build(arrayProperty, sortAscending);
lastKnownKeysHash = GetKeysContentHash(arrayProperty);
- TryRefreshDuplicateIndicesInto(dictionaryProperty, duplicateEntryIndices);
+ TryRefreshDuplicateAndNullKeyIndicesInto(dictionaryProperty, duplicateEntryIndices, nullKeyEntryIndices);
treeView.Reload();
if (displayedItemCount <= 0 || newSelectedDisplayIndex < 0)
@@ -1010,7 +1307,7 @@ void RemoveSelectedEntries()
else
{
int clampedSelection = Mathf.Min(newSelectedDisplayIndex, displayedItemCount - 1);
- treeView.SetSelection(new[] { clampedSelection }, TreeViewSelectionOptions.RevealAndFrame);
+ SelectAndFrameAfterRebuild(new[] { clampedSelection });
needsTreeViewFocus = true;
}
}
@@ -1051,7 +1348,12 @@ void PerformReload()
int currentSize = arrayProperty.arraySize;
sortedIndices = SortedIndexMap.Build(arrayProperty, sortAscending);
lastKnownKeysHash = GetKeysContentHash(arrayProperty);
- TryRefreshDuplicateIndicesInto(dictionaryProperty, duplicateEntryIndices);
+ TryRefreshDuplicateAndNullKeyIndicesInto(dictionaryProperty, duplicateEntryIndices, nullKeyEntryIndices);
+ // Classification only sets flags (which cells are dynamic); it never measures, so it
+ // is safe here in the deferred (container-less) path. It must run before Reload so
+ // InitializeLazyHeights allocates per-row tracking for a dictionary that became
+ // dynamic on this reload (e.g. its first entry was just added). The measurement that
+ // depends on these flags is deferred to the OnGUI Layout pass (GetExpandedPropertyHeight).
if (needsHeightClassification && currentSize > 0)
ClassifyRowHeights();
treeView.Reload();
@@ -1079,8 +1381,16 @@ void RevealSelectionAfterSort(int[] selectedArrayIndices)
if (sortedIndices.ContainsArrayIndex(arrayIdx))
newSelection.Add(sortedIndices.ToDisplayIndex(arrayIdx));
}
- if (newSelection.Count > 0)
- treeView.SetSelection(newSelection, TreeViewSelectionOptions.RevealAndFrame);
+ SelectAndFrameAfterRebuild(newSelection);
+ }
+
+ void SelectAndFrameAfterRebuild(IList displayIndices)
+ {
+ if (displayIndices.Count == 0)
+ return;
+
+ treeView.SetSelection(displayIndices);
+ pendingFrameDisplayIndex = displayIndices[displayIndices.Count - 1];
}
void ResetToDefaults()
@@ -1089,10 +1399,37 @@ void ResetToDefaults()
sortAscending = true;
header.ResetToDefaultFraction(attributeKeyFraction);
+ // Cache was just cleared, so the active layout reverts to the attribute default.
+ layout = attributeLayout;
sortedIndices = SortedIndexMap.Build(arrayProperty, sortAscending);
lastKnownKeysHash = GetKeysContentHash(arrayProperty);
+ ClassifyRowHeights();
treeView.Reload();
+ RepaintForHeightChange();
+ }
+
+ // Single entry point for every layout change from the context menu. Persists the
+ // user's choice (layoutSetByUser) so it wins over the attribute default, then
+ // reclassifies row heights and reloads the tree.
+ void SetLayout(DictionaryLayout newLayout)
+ {
+ if (layout == newLayout)
+ return;
+ layout = newLayout;
+ UpdateCachedState(stateCacheKey, state =>
+ {
+ state.layout = newLayout;
+ state.layoutSetByUser = true;
+ });
+ ApplyLayoutModeChange();
+ }
+
+ void ApplyLayoutModeChange()
+ {
+ ClassifyRowHeights();
+ treeView.Reload();
+ RepaintForHeightChange();
}
static int[] MapSelectionToArrayIndices(IList displayIndices, SortedIndexMap sortedIndices)
@@ -1114,6 +1451,8 @@ public class DictionaryHeader
readonly Hash128 m_StateCacheKey;
readonly GUIContent m_KeyLabel;
readonly GUIContent m_ValueLabel;
+ // One-column mode stacks key over value, so the single header spans both.
+ readonly GUIContent m_OneColumnLabel;
public float height => EditorGUIUtility.singleLineHeight + 2f;
public bool HasCachedState => DictionaryDrawer.HasCachedState(m_StateCacheKey);
@@ -1128,6 +1467,7 @@ public DictionaryHeader(string keyLabel, string valueLabel, float initialFractio
{
m_KeyLabel = new GUIContent(keyLabel);
m_ValueLabel = new GUIContent(valueLabel);
+ m_OneColumnLabel = new GUIContent(Texts.GetOneColumnHeaderLabel(keyLabel, valueLabel));
m_Column1Fraction = initialFraction;
m_ResizeHandleControlID = GUIUtility.GetPermanentControlID();
m_SortToggleControlID = GUIUtility.GetPermanentControlID();
@@ -1142,6 +1482,14 @@ public void OnGUI(Rect rect, DrawerInstanceIMGUI instance)
instance.treeView.SetFocus();
}
+ if (instance.oneColumnMode)
+ {
+ DrawOneColumnHeader(rect, instance);
+ HandleContextMenu(rect, instance);
+ HandleSortToggle(rect, instance);
+ return;
+ }
+
// Use the effective dictionary width (floored at k_MinDictionaryPixelWidth)
// so col1Width can never collapse to zero/negative. The header overflows the
// inspector to the right when rect.width is below the floor, matching the
@@ -1159,7 +1507,7 @@ public void OnGUI(Rect rect, DrawerInstanceIMGUI instance)
GUI.Label(label0Rect, m_KeyLabel, Styles.columnLabelClipped);
var arrowIcon = instance.sortAscending ? Styles.sortAscIcon : Styles.sortDescIcon;
- if (arrowIcon != null)
+ if (arrowIcon != null && !DictionaryDrawer.ShowSerializedOrder)
{
var arrowRect = new Rect(arrowX, label0Rect.y + (label0Rect.height - Styles.k_SortArrowSize) * 0.5f, Styles.k_SortArrowSize, Styles.k_SortArrowSize);
GUI.DrawTexture(arrowRect, arrowIcon);
@@ -1184,6 +1532,21 @@ public void OnGUI(Rect rect, DrawerInstanceIMGUI instance)
HandleSortToggle(col0ButtonRect, instance);
}
+ void DrawOneColumnHeader(Rect rect, DrawerInstanceIMGUI instance)
+ {
+ float keyLabelInset = Styles.k_KeyLeftMargin;
+ float arrowX = rect.xMax - Styles.k_SortArrowSize - 5f;
+ var label0Rect = new Rect(rect.x + keyLabelInset, rect.y, arrowX - (rect.x + keyLabelInset) - 2f, rect.height);
+ GUI.Label(label0Rect, m_OneColumnLabel, Styles.columnLabelClipped);
+
+ var arrowIcon = instance.sortAscending ? Styles.sortAscIcon : Styles.sortDescIcon;
+ if (arrowIcon != null && !DictionaryDrawer.ShowSerializedOrder)
+ {
+ var arrowRect = new Rect(arrowX, rect.y + (rect.height - Styles.k_SortArrowSize) * 0.5f, Styles.k_SortArrowSize, Styles.k_SortArrowSize);
+ GUI.DrawTexture(arrowRect, arrowIcon);
+ }
+ }
+
public void GetColumnRects(Rect rowRect, out Rect col0Rect, out Rect col1Rect)
{
GetColumnPixelWidths(m_Column1Fraction, rowRect.width, out var col0Width, out var col1Width);
@@ -1212,6 +1575,11 @@ void HandleResize(Rect headerRect, float totalWidth, Rect handleRect)
// floor when the new totalWidth would force a column under it.
float newCol0Fraction = (evt.mousePosition.x - headerRect.x) / totalWidth;
m_Column1Fraction = ClampDraggedKeyColumnFraction(newCol0Fraction, totalWidth);
+ var state = GetCachedState(m_StateCacheKey);
+ if (state != null)
+ state.keyColumnFractionSetByUser = m_Column1Fraction;
+ else
+ UpdateCachedState(m_StateCacheKey, cached => cached.keyColumnFractionSetByUser = m_Column1Fraction);
evt.Use();
}
break;
@@ -1228,6 +1596,9 @@ void HandleResize(Rect headerRect, float totalWidth, Rect handleRect)
void HandleSortToggle(Rect sortRect, DrawerInstanceIMGUI instance)
{
+ if (DictionaryDrawer.ShowSerializedOrder)
+ return;
+
var evt = Event.current;
switch (evt.GetTypeForControl(m_SortToggleControlID))
{
@@ -1269,6 +1640,11 @@ public void PersistSortOrder(bool sortAscending)
UpdateCachedState(m_StateCacheKey, cached => cached.sortAscending = sortAscending);
}
+ static void AddLayoutItem(GenericMenu menu, string label, DictionaryLayout layout, DrawerInstanceIMGUI instance)
+ {
+ menu.AddItem(new GUIContent(label), instance.layout == layout, () => instance.SetLayout(layout));
+ }
+
static void HandleContextMenu(Rect headerRect, DrawerInstanceIMGUI instance)
{
var evt = Event.current;
@@ -1276,6 +1652,17 @@ static void HandleContextMenu(Rect headerRect, DrawerInstanceIMGUI instance)
{
var menu = new GenericMenu();
+ // The three layouts form a radio group (the active one is checked),
+ // followed by a separator and the "Reset to Defaults" action.
+ AddLayoutItem(menu, Texts.TwoColumnsLayoutLabel, DictionaryLayout.TwoColumns, instance);
+ AddLayoutItem(menu, Texts.OneColumnWithValueFoldoutLayoutLabel, DictionaryLayout.OneColumnWithValueFoldout, instance);
+ AddLayoutItem(menu, Texts.OneColumnWithValueVisibleLayoutLabel, DictionaryLayout.OneColumnWithValueVisible, instance);
+ menu.AddSeparator(string.Empty);
+
+ menu.AddItem(new GUIContent(Texts.ShowSerializedOrderLabel), DictionaryDrawer.ShowSerializedOrder,
+ () => DictionaryDrawer.SetShowSerializedOrder(!DictionaryDrawer.ShowSerializedOrder));
+ menu.AddSeparator(string.Empty);
+
if (instance.header.HasCachedState)
{
menu.AddItem(new GUIContent(Texts.ResetToDefaultsLabel), false, () =>
@@ -1298,11 +1685,13 @@ public class DictionaryTreeView : TreeView
{
readonly DrawerInstanceIMGUI m_Instance;
- // Per-row measured heights; -1 = unmeasured. Allocated only when variableRowHeight is true.
- // Populated lazily by RowGUI; unmeasured rows use m_EstimatedRowHeight so totalHeight is
- // approximately correct before all rows have painted. Stale entries are caught by
- // RecordDynamicRowHeight setting needsHeightRefresh on the owning instance when a measured
- // row drifts.
+ // Per-row measured heights; -1 = unmeasured. Allocated only when variableRowHeight is
+ // true. Filled by MeasureAllRowHeights on the Layout pass after a reload so totalHeight
+ // is exact (a partially-measured total is unstable and fights the scroll view's clamp —
+ // see GetExpandedPropertyHeight). RowGUI keeps entries current via RecordRowHeight
+ // for runtime height changes (e.g. expanding an inline nested dictionary). Any row left
+ // unmeasured falls back to m_EstimatedRowHeight (the tallest measured row) in
+ // GetCustomRowHeight.
float[] m_LazyHeights;
float m_EstimatedRowHeight;
@@ -1319,36 +1708,73 @@ public void ComputeFixedInlineRowHeight()
var element = m_Instance.arrayProperty.GetArrayElementAtIndex(0);
GetKeyAndValueProperties(element, out var keyProp, out var valueProp);
- float keyH = GetPropertyFieldHeight(keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
- float valH = GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ // Static-inline rows are uniform, so row 0 represents them all.
+ // MeasureRowContentHeight accounts for the active layout — stacked key-over-value
+ // in one-column mode, max(key, value) in two-column — so the fixed height is
+ // correct for both.
+ rowHeight = MeasureRowContentHeight(keyProp, valueProp) + Styles.k_RowVerticalPadding * 2;
EditorGUIUtility.wideMode = prevWideMode;
-
- rowHeight = Mathf.Max(keyH, valH) + Styles.k_RowVerticalPadding * 2;
}
- void InitializeLazyHeights()
+ // Measures every variable-height row into m_LazyHeights so totalHeight is exact and
+ // stable. We measure all rows (not just a sample) because the TreeView scroll view
+ // re-clamps scrollPos to (totalHeight - viewport) every frame: a totalHeight that
+ // keeps shrinking as rows are measured one-at-a-time would repeatedly clamp the scroll
+ // and walk it away from a framed row. Measures cell heights via GetPropertyHeight, so
+ // it must run inside an OnGUI pass — called from GetExpandedPropertyHeight on the
+ // Layout pass, never from the deferred reload.
+ public void MeasureAllRowHeights()
{
int count = m_Instance.displayedItemCount;
- if (!m_Instance.variableRowHeight || count == 0)
- {
- m_LazyHeights = null;
+ if (count == 0)
return;
- }
+
+ if (m_LazyHeights == null || m_LazyHeights.Length != count)
+ m_LazyHeights = new float[count];
bool prevWideMode = EditorGUIUtility.wideMode;
EditorGUIUtility.wideMode = true;
- var element = m_Instance.arrayProperty.GetArrayElementAtIndex(0);
- GetKeyAndValueProperties(element, out var keyProp, out var valueProp);
-
- float keyH = GetPropertyFieldHeight(keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
- float valH = GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ float maxH = 0f;
+ for (int displayIndex = 0; displayIndex < count; displayIndex++)
+ {
+ float h;
+ if (TryGetEntryProperties(displayIndex, out var keyProp, out var valueProp, out _))
+ {
+ h = MeasureRowContentHeight(keyProp, valueProp) + Styles.k_RowVerticalPadding * 2;
+ }
+ else
+ {
+ h = rowHeight;
+ }
+ m_LazyHeights[displayIndex] = h;
+ if (h > maxH)
+ maxH = h;
+ }
EditorGUIUtility.wideMode = prevWideMode;
- m_EstimatedRowHeight = Mathf.Max(keyH, valH) + Styles.k_RowVerticalPadding * 2;
+ m_EstimatedRowHeight = maxH;
+ }
+
+ // Structure only — never measures. BuildRoot/Reload run from the deferred reload
+ // (delayCall / update) with no IMGUIContainer on the stack, so per-row heights (which
+ // for a custom-drawer cell call into that drawer's GetPropertyHeight) are measured
+ // later on the OnGUI Layout pass via MeasureAllRowHeights. Until then unmeasured rows
+ // fall back to the default rowHeight in GetCustomRowHeight.
+ void InitializeLazyHeights()
+ {
+ m_Instance.needsHeightMeasure = true;
+ int count = m_Instance.displayedItemCount;
+ if (!m_Instance.variableRowHeight || count == 0)
+ {
+ m_LazyHeights = null;
+ return;
+ }
+
+ m_EstimatedRowHeight = 0f;
m_LazyHeights = new float[count];
for (int i = 0; i < count; i++)
m_LazyHeights[i] = -1f;
@@ -1404,7 +1830,7 @@ static float GetPropertyFieldHeight(SerializedProperty prop, Type type, bool has
if (prop == null)
return EditorGUIUtility.singleLineHeight;
- if (prop.propertyType == SerializedPropertyType.Generic && !hasCustomDrawer)
+ if (ShouldInlineChildren(prop, hasCustomDrawer))
return GetInlineChildrenHeight(prop);
return EditorGUI.GetPropertyHeight(prop, GUIContent.none, true);
@@ -1439,6 +1865,8 @@ static float GetInlineChildrenHeight(SerializedProperty parent)
protected override void BeforeRowsGUI()
{
base.BeforeRowsGUI();
+ if (m_Instance.oneColumnMode)
+ return;
GetColumnPixelWidths(m_Instance.header.column1Fraction, treeViewRect.width, out var col0Width, out _);
Rect lineRect = new Rect(col0Width, 0, k_VerticalSplitterWidth, totalHeight);
EditorGUI.DrawRect(lineRect, SharedStyles.k_RowsSplitColor);
@@ -1463,17 +1891,28 @@ protected override void RowGUI(RowGUIArgs args)
// use the full treeview rect width. This is needed when the dictionary drawer is
// overflowing in narrow Inspectors.
Rect fullRowRect = new Rect(args.rowRect.x, args.rowRect.y, treeViewRect.width, args.rowRect.height);
- m_Instance.header.GetColumnRects(fullRowRect, out var keyRect, out var valueRect);
- if (showingVerticalScrollBar)
+ if (m_Instance.oneColumnMode)
{
- float visibleRightEdge = fullRowRect.xMax - Styles.k_VerticalScrollbarWidth;
- valueRect.xMax = Mathf.Max(valueRect.xMin, visibleRightEdge);
+ Rect oneColumnRect = fullRowRect;
+ if (showingVerticalScrollBar)
+ oneColumnRect.width = Mathf.Max(0f, oneColumnRect.width - Styles.k_VerticalScrollbarWidth);
+ DrawOneColumnRow(oneColumnRect, keyProp, valueProp, arrayIndex);
}
+ else
+ {
+ m_Instance.header.GetColumnRects(fullRowRect, out var keyRect, out var valueRect);
- DrawKeyCell(keyRect, keyProp, arrayIndex);
- DrawValueCell(valueRect, valueProp);
- RecordDynamicRowHeight(displayIndex, args.rowRect.height, keyProp, valueProp);
+ if (showingVerticalScrollBar)
+ {
+ float visibleRightEdge = fullRowRect.xMax - Styles.k_VerticalScrollbarWidth;
+ valueRect.xMax = Mathf.Max(valueRect.xMin, visibleRightEdge);
+ }
+
+ DrawKeyCell(keyRect, keyProp, arrayIndex);
+ DrawValueCell(valueRect, valueProp);
+ }
+ RecordRowHeight(displayIndex, args.rowRect.height, keyProp, valueProp);
EditorGUIUtility.labelWidth = prevLabelWidth;
EditorGUIUtility.wideMode = prevWideMode;
@@ -1535,21 +1974,35 @@ bool TryGetEntryProperties(int displayIndex, out SerializedProperty keyProp, out
return true;
}
+ // Selects the entry a drop lands on (before the key ObjectField Uses() the DragPerform), so the deferred re-sort keeps it selected and frames it. Mirrors DictionaryView.OnRowDragPerform.
+ void SelectEntryOnKeyDrop(Rect keyCellRect, int arrayIndex)
+ {
+ if (Event.current.type != EventType.DragPerform || !keyCellRect.Contains(Event.current.mousePosition))
+ return;
+ if (!m_Instance.sortedIndices.ContainsArrayIndex(arrayIndex))
+ return;
+ SetSelection(new[] { m_Instance.sortedIndices.ToDisplayIndex(arrayIndex) });
+
+ // Grab view focus (a drag from another view left focus there) so the relocated row shows the active blue outline, not grey; keyboardControl is set by the reload's needsTreeViewFocus path.
+ GUIView.current?.Focus();
+ }
+
void DrawKeyCell(Rect keyRect, SerializedProperty keyProp, int arrayIndex)
{
keyRect.yMin += Styles.k_RowVerticalPadding;
keyRect.yMax -= Styles.k_RowVerticalPadding;
- var markerKind = DictionaryKeyUtility.GetMarkerKind(arrayIndex, m_Instance.duplicateEntryIndices);
+ var markerKind = DictionaryKeyUtility.GetMarkerKind(arrayIndex, m_Instance.duplicateEntryIndices, m_Instance.nullKeyEntryIndices);
if (markerKind != DictionaryKeyUtility.KeyMarkerKind.None)
- DrawDuplicateKeyIcon(keyRect);
+ DrawKeyWarningIcon(keyRect, DictionaryKeyUtility.GetMarkerTooltip(markerKind));
float keyLeft = Styles.k_KeyLeftMargin;
float minFieldWidth = GetCellMinFieldWidth(keyProp, m_Instance.keyHasCustomDrawer);
var keyFieldRect = BuildCellFieldRect(keyRect, keyLeft + Styles.k_CellHorizontalPadding, Styles.k_CellHorizontalPadding, minFieldWidth);
EditorGUIUtility.labelWidth = ComputeCellLabelWidth(keyFieldRect.width);
- // Key edits no longer flip needsReload / needsDuplicateRefresh from here.
+ // Key edits no longer flip needsReload / needsMarkerRefresh from here.
// The TrackPropertyValue listener registered on the IMGUIContainer in
// GetOrCreate handles both same-inspector and cross-inspector
// updates uniformly, so this draw site only renders the field.
+ SelectEntryOnKeyDrop(keyRect, arrayIndex);
DrawClippedPropertyField(keyRect, keyFieldRect, keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
}
@@ -1560,7 +2013,90 @@ void DrawValueCell(Rect cellRect, SerializedProperty valueProp)
float minFieldWidth = GetCellMinFieldWidth(valueProp, m_Instance.valueHasCustomDrawer);
var fieldRect = BuildCellFieldRect(cellRect, Styles.k_ValueLeftPadding, Styles.k_CellHorizontalPadding, minFieldWidth);
EditorGUIUtility.labelWidth = ComputeCellLabelWidth(fieldRect.width);
- DrawPropertyField(fieldRect, valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ DrawPropertyField(fieldRect, valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer, m_Instance.valueCollectionLabel);
+ }
+
+ void DrawOneColumnRow(Rect rowRect, SerializedProperty keyProp, SerializedProperty valueProp, int arrayIndex)
+ {
+ float spacing = EditorGUIUtility.standardVerticalSpacing;
+ float contentLeft = Styles.k_KeyLeftMargin + Styles.k_CellHorizontalPadding;
+ float y = rowRect.y + Styles.k_RowVerticalPadding;
+
+ float keyH = GetPropertyFieldHeight(keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
+ var keyCellRect = new Rect(rowRect.x, y, rowRect.width, keyH);
+ var markerKind = DictionaryKeyUtility.GetMarkerKind(arrayIndex, m_Instance.duplicateEntryIndices, m_Instance.nullKeyEntryIndices);
+ if (markerKind != DictionaryKeyUtility.KeyMarkerKind.None)
+ DrawKeyWarningIcon(keyCellRect, DictionaryKeyUtility.GetMarkerTooltip(markerKind));
+ float keyMinFieldWidth = GetCellMinFieldWidth(keyProp, m_Instance.keyHasCustomDrawer);
+ var keyFieldRect = BuildCellFieldRect(keyCellRect, contentLeft, Styles.k_CellHorizontalPadding, keyMinFieldWidth);
+ EditorGUIUtility.labelWidth = ComputeCellLabelWidth(keyFieldRect.width);
+ SelectEntryOnKeyDrop(keyCellRect, arrayIndex);
+ DrawPropertyField(keyFieldRect, keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
+ y += keyH + spacing;
+
+ if (!m_Instance.useValueFoldouts)
+ y += Styles.k_StaticValueHeaderTopMargin;
+
+ float headerLine = EditorGUIUtility.singleLineHeight;
+ bool showValue = !m_Instance.useValueFoldouts || (valueProp != null && valueProp.isExpanded);
+ float labelX = rowRect.x + contentLeft;
+ float labelWidth = Mathf.Max(0f, rowRect.xMax - Styles.k_CellHorizontalPadding - labelX);
+ var labelRect = EditorGUI.IndentedRect(new Rect(labelX, y, labelWidth, headerLine));
+ if (m_Instance.useValueFoldouts)
+ {
+ bool expanded = valueProp != null && valueProp.isExpanded;
+ const float foldoutArrowAdjustment = 4f; // Move foldout arrow out to align with the key warning icon in the gutter. Still clickable in entire label width
+ Rect foldoutRect = new Rect(labelRect.x - foldoutArrowAdjustment, labelRect.y, labelRect.width + foldoutArrowAdjustment, labelRect.height);
+ bool newExpanded = EditorGUI.Foldout(foldoutRect, expanded, GUIContent.none, true);
+
+ var e = Event.current;
+ if (e.type == EventType.MouseDown && e.button == 0 && foldoutRect.Contains(e.mousePosition))
+ {
+ newExpanded = !expanded;
+ e.Use();
+ }
+
+ if (newExpanded != expanded && valueProp != null)
+ valueProp.isExpanded = newExpanded;
+ showValue = newExpanded;
+ }
+ GUI.Label(labelRect, m_Instance.valueLabelContent, EditorStyles.boldLabel);
+ y += headerLine;
+
+ if (showValue)
+ {
+ y += spacing;
+ float valH = GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ var valueCellRect = new Rect(rowRect.x, y, rowRect.width, valH);
+ float valueMinFieldWidth = GetCellMinFieldWidth(valueProp, m_Instance.valueHasCustomDrawer);
+ float valueIndent = m_Instance.useValueFoldouts ? Styles.k_OneColumnValueIndent : 0f;
+ var valueFieldRect = BuildCellFieldRect(valueCellRect, contentLeft + valueIndent, Styles.k_CellHorizontalPadding, valueMinFieldWidth);
+ EditorGUIUtility.labelWidth = ComputeCellLabelWidth(valueFieldRect.width);
+ DrawPropertyField(valueFieldRect, valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer, m_Instance.valueCollectionLabel);
+ }
+ }
+
+ float MeasureRowContentHeight(SerializedProperty keyProp, SerializedProperty valueProp)
+ {
+ float keyH = GetPropertyFieldHeight(keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
+
+ if (m_Instance.oneColumnMode)
+ {
+ // GetPropertyFieldHeight recurses through every visible child field of the value,
+ // which is wasted work when the value is hidden behind a collapsed foldout — defer
+ // it until we know the value is shown (mirrors the deferral in DrawOneColumnRow).
+ float spacing = EditorGUIUtility.standardVerticalSpacing;
+ float total = keyH + spacing + EditorGUIUtility.singleLineHeight;
+ if (!m_Instance.useValueFoldouts)
+ total += Styles.k_StaticValueHeaderTopMargin;
+ bool showValue = !m_Instance.useValueFoldouts || (valueProp != null && valueProp.isExpanded);
+ if (showValue)
+ total += spacing + GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ return total;
+ }
+
+ float valH = GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
+ return Mathf.Max(keyH, valH);
}
// BuildCellFieldRect floors the field width at a non-zero minimum, so on a narrow
@@ -1591,20 +2127,20 @@ static Rect BuildCellFieldRect(Rect cellRect, float leftPadding, float rightPadd
// Returns the smallest acceptable field-rect width for a cell. The dictionary drawer
// dispatches in DrawPropertyField:
- // - Generic + no custom drawer → DrawInlineChildren expands child properties and
- // each child's PropertyField reserves EditorGUIUtility.labelWidth for its own
- // label (e.g. "Color", "Target", "Vector"). The cell therefore needs room for
- // both a min label and a min control, plus the kPrefixPaddingRight gap that
- // PrefixLabel inserts between them.
- // - Anything else → EditorGUI.PropertyField is called with
- // GUIContent.none, so PrefixLabel's "no label" branch hands the entire rect to
- // the control and labelWidth is irrelevant. Only a min control width is needed.
+ // - inline children (ShouldInlineChildren) → DrawInlineChildren expands child
+ // properties and each child's PropertyField reserves EditorGUIUtility.labelWidth
+ // for its own label (e.g. "Color", "Target", "Vector"). The cell therefore needs
+ // room for both a min label and a min control, plus the kPrefixPaddingRight gap
+ // that PrefixLabel inserts between them.
+ // - anything else (custom drawer, array/list, leaf) → EditorGUI.PropertyField is
+ // called with GUIContent.none, so PrefixLabel's "no label" branch hands the entire
+ // rect to the control and labelWidth is irrelevant. Only a min control width is needed.
// IMGUI labels default to TextClipping.Overflow, so simply setting labelWidth = 0
// does not hide the label — it keeps overflowing onto the control area. Reserving
// the right amount of space up front is the only way to keep both visible.
static float GetCellMinFieldWidth(SerializedProperty prop, bool hasCustomDrawer)
{
- bool willInlineChildren = prop != null && prop.propertyType == SerializedPropertyType.Generic && !hasCustomDrawer;
+ bool willInlineChildren = ShouldInlineChildren(prop, hasCustomDrawer);
return willInlineChildren
? Styles.k_CellLabelMinWidth + EditorGUI.kPrefixPaddingRight + Styles.k_CellControlMinWidth
: Styles.k_CellControlMinWidth;
@@ -1626,20 +2162,34 @@ static float ComputeCellLabelWidth(float fieldRectWidth)
return Mathf.Clamp(desired, Styles.k_CellLabelMinWidth, maxLabel);
}
- void RecordDynamicRowHeight(int displayIndex, float currentRowHeight, SerializedProperty keyProp, SerializedProperty valueProp)
+ void RecordRowHeight(int displayIndex, float currentRowHeight, SerializedProperty keyProp, SerializedProperty valueProp)
{
- if (!m_Instance.dynamicRowHeight)
+ if (!m_Instance.dynamicRowHeight && !m_Instance.hasStaticInlineHeight)
return;
- float keyH = GetPropertyFieldHeight(keyProp, m_Instance.keyType, m_Instance.keyHasCustomDrawer);
- float valH = GetPropertyFieldHeight(valueProp, m_Instance.valueType, m_Instance.valueHasCustomDrawer);
- float measuredH = Mathf.Max(keyH, valH) + Styles.k_RowVerticalPadding * 2;
+ float measuredH = MeasureRowContentHeight(keyProp, valueProp) + Styles.k_RowVerticalPadding * 2;
- if (m_LazyHeights != null && displayIndex >= 0 && displayIndex < m_LazyHeights.Length)
- m_LazyHeights[displayIndex] = measuredH;
+ if (m_Instance.dynamicRowHeight)
+ {
+ if (m_LazyHeights != null && displayIndex >= 0 && displayIndex < m_LazyHeights.Length)
+ m_LazyHeights[displayIndex] = measuredH;
+ }
+ else
+ {
+ rowHeight = measuredH;
+ }
if (!m_Instance.needsHeightRefresh && Mathf.Abs(currentRowHeight - measuredH) > 0.5f)
+ {
m_Instance.needsHeightRefresh = true;
+ // The row was drawn at the wrong height (it used the estimate, or a child's
+ // height just changed — e.g. a nested dictionary expanded, or a row scrolled
+ // into view for the first time). The fix (RefreshCustomRowHeights) is applied
+ // on the next Layout pass in GetExpandedPropertyHeight, so we must request a
+ // repaint to make that pass happen; otherwise the rows only reflow when some
+ // unrelated event (mouse move) repaints the inspector.
+ HandleUtility.Repaint();
+ }
}
void DrawRowSelectionOutlineIfSelected(RowGUIArgs args)
@@ -1661,18 +2211,34 @@ static void DrawSelectionOutline(Rect rect, bool focused)
EditorGUI.DrawRect(new Rect(rect.xMax - w, rect.y + w, w, rect.height - 2 * w), color);
}
- static void DrawPropertyField(Rect rect, SerializedProperty prop, Type type, bool hasCustomDrawer)
+ // A cell expands its children inline (DrawInlineChildren) only for a plain serializable
+ // compound: Generic, no custom drawer, and NOT an array/list. Arrays and lists are
+ // Generic too, but must go through EditorGUI.PropertyField so they get their real
+ // drawer (foldout + size + element list / reorderable list). Iterating their raw
+ // children instead would draw the hidden size field and the elements flat, with no
+ // foldout and no way to resize — which is how array/list cells were rendering wrong.
+ static bool ShouldInlineChildren(SerializedProperty prop, bool hasCustomDrawer)
+ {
+ return prop != null
+ && prop.propertyType == SerializedPropertyType.Generic
+ && !hasCustomDrawer
+ && !prop.isArray;
+ }
+
+ static void DrawPropertyField(Rect rect, SerializedProperty prop, Type type, bool hasCustomDrawer, GUIContent label = null)
{
if (prop == null)
return;
- if (prop.propertyType == SerializedPropertyType.Generic && !hasCustomDrawer)
+ if (ShouldInlineChildren(prop, hasCustomDrawer))
{
DrawInlineChildren(rect, prop);
}
else
{
- EditorGUI.PropertyField(rect, prop, GUIContent.none, true);
+ // label is non-null only for collection value cells ("Array"/"List"); everything else
+ // draws label-less so the value fills the cell (a nested dictionary supplies its own title).
+ EditorGUI.PropertyField(rect, prop, label ?? GUIContent.none, true);
}
}
@@ -1700,23 +2266,23 @@ static void DrawInlineChildren(Rect rect, SerializedProperty parent)
}
// Draws a fixed-size warning icon at the top of the key column gutter for
- // rows whose key is a duplicate. Position and size mirror UITK
- // .unity-dictionary-view__duplicate-key-icon so both backends look identical. The
+ // rows excluded from the runtime dictionary (duplicate or null key). Position and
+ // size mirror UITK .unity-dictionary-view__duplicate-key-icon so both backends look identical. The
// GUI.Label call paints nothing on its own (GUIStyle.none + empty text) but
// registers the hit area for the hover tooltip.
- static void DrawDuplicateKeyIcon(Rect cellRect)
+ static void DrawKeyWarningIcon(Rect cellRect, string tooltip)
{
var icon = EditorGUIUtility.GetHelpIcon(MessageType.Warning);
if (icon == null)
return;
var iconRect = new Rect(
- cellRect.x + Styles.k_DuplicateKeyIconLeftMargin,
- cellRect.y + Styles.k_DuplicateKeyIconTopOffset,
- Styles.k_DuplicateKeyIconSize,
- Styles.k_DuplicateKeyIconSize);
+ cellRect.x + Styles.k_KeyWarningIconLeftMargin,
+ cellRect.y + Styles.k_KeyWarningIconTopOffset,
+ Styles.k_KeyWarningIconSize,
+ Styles.k_KeyWarningIconSize);
GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit);
- GUI.Label(iconRect, EditorGUIUtility.TempContent(string.Empty, Texts.DuplicateMarkerTooltip), GUIStyle.none);
+ GUI.Label(iconRect, EditorGUIUtility.TempContent(string.Empty, tooltip), GUIStyle.none);
}
protected override bool CanMultiSelect(TreeViewItem item)
@@ -1770,6 +2336,7 @@ internal static class DrawerEditorGUI
// same style with UpperLeft so the content stays anchored to the top, leaving the
// bottom-right corner clear for the button overlay. Lazy-init: EditorStyles.helpBox
// is not safe to access during static class reload.
+ [NoAutoStaticsCleanup] // lazy GUIStyle cache guarded by == null; re-created on first access, safe to persist
static GUIStyle s_HelpBoxUpperLeft;
static GUIStyle helpBoxUpperLeft
{
diff --git a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerShared.cs b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerShared.cs
index 5018662f5c..a3b60489a7 100644
--- a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerShared.cs
+++ b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerShared.cs
@@ -7,7 +7,10 @@
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
+using UnityEngine.Assemblies;
using UnityEngine.UIElements;
+using Unity.Scripting.LifecycleManagement;
+using UnityEngine.Pool;
namespace UnityEditor
{
@@ -65,22 +68,27 @@ internal partial class DictionaryDrawer
{
internal static class SharedStyles
{
+ [NoAutoStaticsCleanup] // skinned color constant; skin is session-fixed, safe to persist
internal static readonly EditorGUIUtility.SkinnedColor k_RowsSplitColor = new EditorGUIUtility.SkinnedColor(
new Color(137f / 255f, 137f / 255f, 137f / 255f, 0.3f),
new Color(36f / 255f, 36f / 255f, 36f / 255f, 0.5f));
+ [NoAutoStaticsCleanup] // skinned color constant; skin is session-fixed, safe to persist
internal static readonly EditorGUIUtility.SkinnedColor k_ResizerColor = new EditorGUIUtility.SkinnedColor(
new Color(137f / 255f, 137f / 255f, 137f / 255f, 0.3f),
new Color(36f / 255f, 36f / 255f, 36f / 255f, 0.8f));
+ [NoAutoStaticsCleanup] // skinned color constant; skin is session-fixed, safe to persist
internal static readonly EditorGUIUtility.SkinnedColor k_AlternatingRowColor = new EditorGUIUtility.SkinnedColor(
new Color(0f, 0f, 0f, 0.07f),
new Color(0f, 0f, 0f, 0.04f));
+ [NoAutoStaticsCleanup] // skinned color constant; skin is session-fixed, safe to persist
internal static readonly EditorGUIUtility.SkinnedColor k_SelectionOutlineColor = new EditorGUIUtility.SkinnedColor(
new Color(58f / 255f, 114f / 255f, 176f / 255f),
new Color(44f / 255f, 93f / 255f, 135f / 255f));
+ [NoAutoStaticsCleanup] // skinned color constant; skin is session-fixed, safe to persist
internal static readonly EditorGUIUtility.SkinnedColor k_SelectionOutlineColorInactive = new EditorGUIUtility.SkinnedColor(
new Color(174f / 255f, 174f / 255f, 174f / 255f),
new Color(77f / 255f, 77f / 255f, 77f / 255f));
@@ -89,20 +97,41 @@ internal static class SharedStyles
internal static class Texts
{
internal static readonly string EmptyDictionaryLabel = L10n.Tr("Dictionary is empty");
+ // Foldout title for a nested dictionary, replacing its entry "value" field's displayName ("Value").
+ internal static readonly string NestedDictionaryLabel = L10n.Tr("Dictionary");
+ // Foldout titles for a dictionary value that is itself a collection, replacing the "Value" displayName.
+ internal static readonly string NestedArrayLabel = L10n.Tr("Array");
+ internal static readonly string NestedListLabel = L10n.Tr("List");
internal static readonly string ResetToDefaultsLabel = L10n.Tr("Reset to Defaults");
internal static readonly string MultiEditUnsupportedMessage = L10n.Tr("Dictionary: Multi-object editing is not supported."); // Entries are sorted by key, so a given row may correspond to different entries across targets, so edits could affect unrelated entries
- internal static readonly string DuplicateMarkerTooltip = L10n.Tr("An element with the same key already exists, so this element will not be part of the Dictionary");
+ internal static readonly string DuplicateMarkerTooltip = L10n.Tr("An element with the same key already exists, so this element is excluded from the runtime dictionary.");
+ internal static readonly string NullKeyMarkerTooltip = L10n.Tr("The key is null. A dictionary only stores entries with a valid (non-null) key, so this element is excluded from the runtime dictionary.");
internal static readonly string SingleItemCountLabel = L10n.Tr("1 item");
internal static readonly string MultipleItemsCountFormat = L10n.Tr("{0} items");
- internal static readonly string DuplicatesFormat = L10n.Tr("{0} ignored");
+ internal static readonly string IgnoredFormat = L10n.Tr("{0} ignored");
internal static readonly string DuplicatesHelpBoxSingle = L10n.Tr("1 duplicate key ignored. Ensure all keys are unique.");
internal static readonly string DuplicatesHelpBoxFormat = L10n.Tr("{0} duplicate keys ignored. Ensure all keys are unique.");
- internal static readonly string SelectFirstDuplicateButtonLabel = L10n.Tr("Select Duplicate");
-
- internal static readonly string DefaultKeyLabel = "Key";
- internal static readonly string DefaultValueLabel = "Value";
+ internal static readonly string NullKeysHelpBoxSingle = L10n.Tr("1 null key ignored. A dictionary key can't be null.");
+ internal static readonly string NullKeysHelpBoxFormat = L10n.Tr("{0} null keys ignored. A dictionary key can't be null.");
+ internal static readonly string MixedIgnoredHelpBoxFormat = L10n.Tr("{0} entries ignored. Ensure all keys are unique and non-null.");
+ internal static readonly string SelectFirstIgnoredButtonLabel = L10n.Tr("Select");
+ // Header context-menu labels for the three DictionaryLayout values, shown as a
+ // radio group (the active layout is checked).
+ internal static readonly string TwoColumnsLayoutLabel = L10n.Tr("Two Columns");
+ internal static readonly string OneColumnWithValueFoldoutLayoutLabel = L10n.Tr("One Column With Value Foldout");
+ internal static readonly string OneColumnWithValueVisibleLayoutLabel = L10n.Tr("One Column With Value Visible");
+ internal static readonly string ShowSerializedOrderLabel = L10n.Tr("Show Serialized Order (Global)");
+ internal static readonly string ShowingSerializedOrderInfoLabel = L10n.Tr("Showing serialized order");
+
+ internal static readonly string DefaultKeyLabel = L10n.Tr("Key");
+ internal static readonly string DefaultValueLabel = L10n.Tr("Value");
+ // One-column mode stacks the key and value, so the single header column spans both.
+ internal static readonly string OneColumnHeaderFormat = L10n.Tr("{0} & {1}");
internal static readonly string ExpectedCurrentContainerMessage = "Expected a current IMGUIContainer, please report a bug with repro steps";
+ internal static string GetOneColumnHeaderLabel(string keyLabel, string valueLabel) =>
+ string.Format(OneColumnHeaderFormat, keyLabel, valueLabel);
+
internal static string GetItemCountText(int count)
{
return count == 1
@@ -113,16 +142,27 @@ internal static string GetItemCountText(int count)
// Returned text includes the leading ", " separator so callers can
// unconditionally append it after the item-count text without any
// separator/comma bookkeeping at the call site.
- internal static string GetDuplicateCountText(int count)
+ internal static string GetIgnoredCountText(int ignoredCount)
{
- return ", " + string.Format(DuplicatesFormat, count);
+ return ", " + string.Format(IgnoredFormat, ignoredCount);
}
- internal static string GetDuplicatesHelpBoxText(int count)
+ internal static string GetIgnoredHelpBoxText(int duplicateCount, int nullKeyCount)
{
- return count == 1
+ bool hasDuplicates = duplicateCount > 0;
+ bool hasNullKeys = nullKeyCount > 0;
+
+ if (hasDuplicates && hasNullKeys)
+ return string.Format(MixedIgnoredHelpBoxFormat, duplicateCount + nullKeyCount);
+
+ if (hasNullKeys)
+ return nullKeyCount == 1
+ ? NullKeysHelpBoxSingle
+ : string.Format(NullKeysHelpBoxFormat, nullKeyCount);
+
+ return duplicateCount == 1
? DuplicatesHelpBoxSingle
- : string.Format(DuplicatesHelpBoxFormat, count);
+ : string.Format(DuplicatesHelpBoxFormat, duplicateCount);
}
}
@@ -134,13 +174,28 @@ internal class DictionaryState
// GetActiveKeyColumnFraction falls back to the attribute default in that case.
public float keyColumnFractionSetByUser = -1f;
public bool sortAscending = true;
+ // Single source of truth for the column layout. TwoColumns: key | value side by
+ // side. OneColumnWithValueFoldout: key stacked over value, each value collapsible
+ // behind a "Value" foldout via SerializedProperty.isExpanded. OneColumnWithValueVisible:
+ // same stacking but every value renders inline with no per-row foldout.
+ public DictionaryLayout layout = DictionaryLayout.TwoColumns;
+ // Negative sentinel pattern, mirroring keyColumnFractionSetByUser: false means the
+ // user has never picked a layout from the context menu, so GetActiveLayout falls
+ // back to the attribute default. Set to true the moment the user toggles layout.
+ public bool layoutSetByUser = false;
}
// Disk-backed, persistent across editor sessions. Key: Hash128 of normalized path
- // ([\d+] → []) so list siblings share state (linked resizers). Eviction: only via
+ // ([\d+] → []) so container siblings share state (linked views). Eviction: only via
// explicit RemoveState from the "Reset to Defaults" context menu.
+ [NoAutoStaticsCleanup] // disk-backed state cache, intentionally persistent across sessions; safe to persist
static readonly StateCache s_StateCache = new StateCache("Library/StateCache/DictionaryDrawer/");
+ [NoAutoStaticsCleanup] // session-scoped change counter; value is irrelevant across reloads, only that it changes
+ static int s_StateVersion;
+
+ internal static int StateVersion => s_StateVersion;
+
internal const float k_MinColumnPixelWidth = 40f;
internal const float k_MinDictionaryPixelWidth = 2f * k_MinColumnPixelWidth;
internal const float k_VerticalSplitterWidth = 1f;
@@ -153,6 +208,7 @@ internal class DictionaryState
// Used by tests to assert that certain changes do not trigger a re-sort. Kept on the
// shared so a single counter is shared regardless of the per-property DrawerInstance.
+ [NoAutoStaticsCleanup] // test-only diagnostic counter, reset by tests; safe to persist
internal static int s_SortCount;
// True when totalWidth has hit the floor, i.e. dragging the resizer can't move the
@@ -196,11 +252,29 @@ internal static float ClampDraggedKeyColumnFraction(float keyColumnFraction, flo
}
internal static float GetActiveKeyColumnFraction(Hash128 stateCacheKey, float attributeFraction)
+ => GetActiveKeyColumnFraction(s_StateCache.GetState(stateCacheKey), attributeFraction);
+
+ // Overload for callers that already hold the cached state (e.g. a per-frame sync that reads
+ // several fields), so they resolve all of them from a single GetState lookup.
+ internal static float GetActiveKeyColumnFraction(DictionaryState state, float attributeFraction)
{
- var cached = s_StateCache.GetState(stateCacheKey);
- if (cached == null || cached.keyColumnFractionSetByUser <= 0f)
+ if (state == null || state.keyColumnFractionSetByUser <= 0f)
return attributeFraction;
- return cached.keyColumnFractionSetByUser;
+ return state.keyColumnFractionSetByUser;
+ }
+
+ // Layout follows the same default-vs-override rules as the key column fraction: the
+ // attribute supplies the default, and the cached layout only wins once the user has
+ // explicitly toggled it from the header context menu (layoutSetByUser). "Reset to
+ // Defaults" removes the cache entry, so the attribute default returns.
+ internal static DictionaryLayout GetActiveLayout(Hash128 stateCacheKey, DictionaryLayout attributeLayout)
+ => GetActiveLayout(s_StateCache.GetState(stateCacheKey), attributeLayout);
+
+ internal static DictionaryLayout GetActiveLayout(DictionaryState state, DictionaryLayout attributeLayout)
+ {
+ if (state == null || !state.layoutSetByUser)
+ return attributeLayout;
+ return state.layout;
}
static DictionaryState GetOrCreateCachedState(Hash128 stateCacheKey)
@@ -218,6 +292,7 @@ internal static void UpdateCachedState(Hash128 stateCacheKey, Action SessionState.GetBool(k_SerializedOrderSessionKey, false);
+
+ internal static event Action SerializedOrderChanged;
+
+ internal static void SetShowSerializedOrder(bool value)
+ {
+ if (ShowSerializedOrder == value)
+ return;
+ SessionState.SetBool(k_SerializedOrderSessionKey, value);
+ SerializedOrderChanged?.Invoke();
+ DrawerInstanceIMGUI.InvalidateAllSortOrders();
}
// We want a shared ui state for all dictionaries in lists/arrays, so the user do not have
@@ -240,6 +331,24 @@ internal static Hash128 ComputeStateCacheKey(string propertyPath)
return Hash128.Compute(normalizedPath);
}
+ // Why siblings share state at all: every collection element at a given level intentionally
+ // shares ONE persisted DictionaryState — their paths all normalize to the same
+ // ComputeStateCacheKey. Two reasons:
+ // 1. Avoid an explosion of persisted StateCache objects: without it, resizing a column (or
+ // changing sort/layout) inside a container with 100,000+ elements could write 100,000+
+ // per-element StateCache entries to disk. Collapsing the index means one shared entry per
+ // level regardless of element count.
+ // 2. Consistency, matching how [DictionaryDisplayForType] configures appearance for ALL
+ // dictionaries of a given closed type: every element at the same nested level should look
+ // and behave the same, so the user adjusts sort/layout/column-width once rather than
+ // re-doing it across every one of many nested dictionaries.
+ // Returns true when the property is a collection element (its path carries a numeric [\d+] index
+ // token), so it has sibling dictionaries whose live views should be linked and kept in sync.
+ internal static bool ShouldLinkViewStateWithSiblings(string propertyPath)
+ {
+ return s_ArrayIndexPattern.IsMatch(propertyPath);
+ }
+
static Type[] GetDictionaryGenericArguments(FieldInfo fieldInfo)
{
return fieldInfo.FieldType.GetGenericArguments();
@@ -247,6 +356,7 @@ static Type[] GetDictionaryGenericArguments(FieldInfo fieldInfo)
internal readonly struct SortedIndexMap
{
+ [NoAutoStaticsCleanup] // immutable empty sentinel backed by Array.Empty; safe to persist
public static readonly SortedIndexMap Empty =
new SortedIndexMap(Array.Empty(), Array.Empty());
@@ -264,11 +374,20 @@ internal readonly struct SortedIndexMap
public static SortedIndexMap Build(SerializedProperty arrayProperty, bool ascending)
{
- s_SortCount++;
int n = arrayProperty.arraySize;
if (n == 0)
return Empty;
+ if (DictionaryDrawer.ShowSerializedOrder)
+ {
+ var identity = new int[n];
+ for (int i = 0; i < n; i++)
+ identity[i] = i;
+ return new SortedIndexMap(identity, identity);
+ }
+
+ s_SortCount++;
+
// The native sort flips its key comparison based on `ascending` but always
// breaks ties on the original array index in ascending order. Reversing the
// sorted indices in C# would also flip the tiebreaker, pushing a duplicate
@@ -314,11 +433,20 @@ internal static bool SortedOrderEquals(int[] a, int[] b)
return true;
}
- // Returns true if the set actually changed, so callers can skip
+ // Returns true if either set actually changed, so callers can skip
// UI refreshes (label text, gutter markers) when nothing differs.
- internal static bool TryRefreshDuplicateIndicesInto(SerializedProperty dictionaryProperty, HashSet target)
+ internal static bool TryRefreshDuplicateAndNullKeyIndicesInto(
+ SerializedProperty dictionaryProperty, HashSet duplicateTarget, HashSet nullKeyTarget)
+ {
+ var ignored = dictionaryProperty.GetDictionaryIgnoredEntries();
+ bool duplicatesChanged = TryRefreshIndicesInto(ignored.duplicateEntryIndices, duplicateTarget);
+ bool nullKeysChanged = TryRefreshIndicesInto(ignored.nullKeyEntryIndices, nullKeyTarget);
+ return duplicatesChanged || nullKeysChanged;
+ }
+
+ static bool TryRefreshIndicesInto(int[] newIndices, HashSet target)
{
- var newIndices = dictionaryProperty.GetDictionaryDuplicateEntryIndices() ?? Array.Empty();
+ newIndices ??= Array.Empty();
if (target.Count == newIndices.Length)
{
bool allMatch = true;
@@ -340,27 +468,282 @@ internal static bool TryRefreshDuplicateIndicesInto(SerializedProperty dictionar
return true;
}
- internal static void GetHeaderLabels(FieldInfo fieldInfo, out string keyLabel, out string valueLabel, out float keyColumnFraction)
+ // Resolves the key/value column header labels and default key-column fraction for a dictionary.
+ // These come from the same [DictionaryDisplay] attribute that drives layout: a field-level
+ // attribute on the directly-declared field, or an assembly-level attribute targeting the exact
+ // closed Dictionary. dictionaryType is the property's static closed type, which for a nested
+ // inner dictionary differs from fieldInfo.FieldType (see GetFieldDisplayAttribute).
+ internal static void GetHeaderLabels(FieldInfo fieldInfo, Type dictionaryType, out string keyLabel, out string valueLabel, out float keyColumnFraction)
{
keyLabel = Texts.DefaultKeyLabel;
valueLabel = Texts.DefaultValueLabel;
keyColumnFraction = 0.5f;
- var attr = fieldInfo?.GetCustomAttribute();
- if (attr != null)
+ var fieldAttr = GetFieldDisplayAttribute(fieldInfo, dictionaryType);
+ if (fieldAttr != null)
{
- if (!string.IsNullOrEmpty(attr.keyColumnLabel))
- keyLabel = attr.keyColumnLabel;
- if (!string.IsNullOrEmpty(attr.valueColumnLabel))
- valueLabel = attr.valueColumnLabel;
- // Sanity-clamp only — keeps NaN/<0/>1 attribute values out of the cache.
- // The actual rendered width is enforced by GetKeyColumnPixelWidth, which
- // applies the pixel floor regardless of the stored fraction's exact value.
- var fraction = attr.keyColumnFraction;
- if (float.IsNaN(fraction))
- fraction = 0.5f;
- keyColumnFraction = Mathf.Clamp(fraction, 0.01f, 0.99f);
+ ApplyHeaderLabels(fieldAttr, ref keyLabel, ref valueLabel, ref keyColumnFraction);
+ return;
}
+
+ if (dictionaryType != null && GetAssemblyLayoutRegistry().TryGetValue(dictionaryType, out var entry))
+ ApplyHeaderLabels(entry.attribute, ref keyLabel, ref valueLabel, ref keyColumnFraction);
+ }
+
+ static void ApplyHeaderLabels(DictionaryDisplayAttribute attr, ref string keyLabel, ref string valueLabel, ref float keyColumnFraction)
+ {
+ if (!string.IsNullOrEmpty(attr.keyLabel))
+ keyLabel = attr.keyLabel;
+ if (!string.IsNullOrEmpty(attr.valueLabel))
+ valueLabel = attr.valueLabel;
+ // Sanity-clamp only — keeps NaN/<0/>1 attribute values out of the cache.
+ // The actual rendered width is enforced by GetKeyColumnPixelWidth, which
+ // applies the pixel floor regardless of the stored fraction's exact value.
+ var fraction = attr.keyColumnFraction;
+ if (float.IsNaN(fraction))
+ fraction = 0.5f;
+ keyColumnFraction = Mathf.Clamp(fraction, 0.01f, 0.99f);
+ }
+
+ // Returns the field-level [DictionaryDisplay] that applies to THIS dictionary, or null.
+ // A field attribute only governs the dictionary the field *directly* declares. For a nested
+ // inner dictionary, fieldInfo resolves to the outer field (dict elements are not fields), so
+ // its dictionaryType differs from fieldInfo.FieldType; in that case the field attribute belongs
+ // to the outer dictionary and must not leak onto the inner one (which resolves via the
+ // assembly-level registry instead). The assembly form (DictionaryDisplayForTypeAttribute) is
+ // AttributeTargets.Assembly, so it can never appear on a field — no form check is needed here.
+ static DictionaryDisplayAttribute GetFieldDisplayAttribute(FieldInfo fieldInfo, Type dictionaryType)
+ {
+ if (fieldInfo == null || dictionaryType != fieldInfo.FieldType)
+ return null;
+
+ return fieldInfo.GetCustomAttribute();
+ }
+
+ // Single source of truth for the foldout title of a nested collection value, keyed on its type:
+ // "Dictionary" / "Array" / "List" (add new collection kinds, e.g. HashSet, here). Returns null for
+ // any non-collection type so the value cell stays label-less and the field keeps its "Value" name.
+ //
+ // The enclosing dictionary calls this for its value type and feeds the result to the value cell as a
+ // plain label (see DictionaryView.m_ValueFieldLabel / DrawerInstanceIMGUI.valueCollectionLabel). The
+ // value's own drawer then renders it: a nested dictionary reads it through PropertyField (UITK) or its
+ // OnGUI label (IMGUI), an array/list through its built-in foldout title — so no drawer needs to detect
+ // the nested-value case itself.
+ internal static string GetNestedCollectionValueLabel(Type collectionType)
+ {
+ if (collectionType == null)
+ return null;
+ if (collectionType.IsArray)
+ return Texts.NestedArrayLabel;
+ if (collectionType.IsGenericType)
+ {
+ var definition = collectionType.GetGenericTypeDefinition();
+ if (definition == typeof(Dictionary<,>))
+ return Texts.NestedDictionaryLabel;
+ if (definition == typeof(List<>))
+ return Texts.NestedListLabel;
+ }
+ return null;
+ }
+
+ // Resolves the default layout for a dictionary, applying this precedence:
+ // 1. A field-level [DictionaryDisplay] on the dictionary field (explicit per-field intent).
+ // 2. An assembly-level [DictionaryDisplayForType(typeof(Dictionary), ...)] matching the exact
+ // closed dictionary type — the only way to reach a nested dictionary or a type you don't own.
+ // Matches globally (applies wherever such a dictionary is used), but a rule is only admitted
+ // if its declaring assembly owns K or V — see GetAssemblyLayoutRegistry / DeclaresTargetType.
+ // 3. TwoColumns.
+ // The user's context-menu choice still wins at runtime; GetActiveLayout layers that on top of this.
+ // dictionaryType is the closed Dictionary for this specific field/value (from the property's
+ // static type), which for nested dictionaries differs from fieldInfo.FieldType.
+ internal static DictionaryLayout ResolveDefaultLayout(FieldInfo fieldInfo, Type dictionaryType)
+ {
+ var fieldAttr = GetFieldDisplayAttribute(fieldInfo, dictionaryType);
+ if (fieldAttr != null)
+ return fieldAttr.layout;
+
+ if (dictionaryType != null && GetAssemblyLayoutRegistry().TryGetValue(dictionaryType, out var exact))
+ return exact.attribute.layout;
+
+ return DictionaryLayout.TwoColumns;
+ }
+
+ // True for a closed Dictionary. Used by the registry builder to reject a
+ // [DictionaryDisplayForType] whose target is not a closed dictionary: only exact Dictionary
+ // targets are supported. IsConstructedGenericType (not IsGenericType) is required so the open
+ // typeof(Dictionary<,>) — which has no concrete K/V to match — is rejected rather than silently
+ // registered under a key nothing ever resolves to.
+ static bool IsExactDictionaryType(Type type)
+ => type != null && type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>);
+
+ // One resolved assembly-level entry. Holds the originating attribute so layout, labels, and
+ // fraction all read from the same source without a second reflection pass, plus the name of the
+ // assembly that declared it so a duplicate warning can point back to the winning declaration.
+ readonly struct AssemblyLayoutEntry
+ {
+ public readonly DictionaryDisplayAttribute attribute;
+ public readonly string declaringAssemblyName;
+ public AssemblyLayoutEntry(DictionaryDisplayAttribute attribute, string declaringAssemblyName)
+ {
+ this.attribute = attribute;
+ this.declaringAssemblyName = declaringAssemblyName;
+ }
+ }
+
+ // Lazily-built map of [assembly: DictionaryDisplayForType(targetType, ...)] across all loaded
+ // assemblies. A target must be a closed Dictionary (other shapes are ignored). Matching is
+ // global — a target matches wherever such a dictionary is used — but an assembly may only declare
+ // a rule for a target that involves a type it *defines* somewhere in the dictionary's shape — its
+ // key, its value, or a type nested within either (see DeclaresTargetType). That ownership gate lets
+ // an extension style dictionaries over its own types everywhere they appear, while stopping a rule
+ // for a shape it has no stake in (e.g. Dictionary) from hijacking every such dictionary in
+ // a project. The key is the closed Dictionary targetType.
+ // Statics reset on domain reload, so the cache rebuilds automatically when assemblies change.
+ [AutoStaticsCleanupOnCodeReload]
+ static Dictionary s_AssemblyLayoutRegistry;
+
+ static Dictionary GetAssemblyLayoutRegistry()
+ {
+ if (s_AssemblyLayoutRegistry != null)
+ return s_AssemblyLayoutRegistry;
+
+ var registry = new Dictionary();
+
+ // CurrentAssemblies.GetLoadedAssemblies() is the Unity-safe enumeration (AppDomain.GetAssemblies
+ // can return already-unloaded assemblies — analyzer UAC0005). Sort by name so a target that is
+ // legitimately owned by more than one assembly (K and V authored in different assemblies)
+ // resolves deterministically (first by assembly name wins).
+ var assemblies = new List(CurrentAssemblies.GetLoadedAssemblies());
+ assemblies.Sort((a, b) => string.CompareOrdinal(a.FullName, b.FullName));
+
+ foreach (var assembly in assemblies)
+ {
+ object[] attrs;
+ try
+ {
+ attrs = assembly.GetCustomAttributes(typeof(DictionaryDisplayForTypeAttribute), false);
+ }
+ catch
+ {
+ // A dynamic or otherwise reflection-hostile assembly: skip it.
+ continue;
+ }
+
+ foreach (DictionaryDisplayForTypeAttribute attr in attrs)
+ {
+ var target = attr.targetType;
+
+ // Only exact closed Dictionary targets are supported.
+ if (!IsExactDictionaryType(target))
+ {
+ Debug.LogWarning($"The DictionaryDisplayForType attribute targeting {FormatType(target)} in {assembly.GetName().Name} is ignored: it must target a Dictionary, for example typeof(Dictionary).");
+ continue;
+ }
+
+ // Ownership gate: reject a rule whose target involves no type defined in this assembly.
+ if (!DeclaresTargetType(target, assembly))
+ {
+ Debug.LogWarning(OwnershipRejectionMessage(target, assembly));
+ continue;
+ }
+
+ // Duplicate rules for the same target: keep the first and warn. Note this only sees
+ // duplicates that survive to metadata — two byte-for-byte identical attributes in one
+ // assembly (same ctor args and same named args) are folded into a single entry by the
+ // C# compiler, so they never reach here and cannot be warned about at runtime. What we
+ // do catch: same-assembly duplicates that differ in any setting, and any cross-assembly
+ // duplicate (identical or not, since each assembly contributes its own metadata entry).
+ if (registry.TryGetValue(target, out var existing))
+ {
+ var assemblyName = assembly.GetName().Name;
+ var where = existing.declaringAssemblyName == assemblyName
+ ? $"is declared more than once in {assemblyName}"
+ : $"is declared more than once: kept the rule from {existing.declaringAssemblyName} and ignored the one from {assemblyName}";
+ Debug.LogWarning($"The DictionaryDisplayForType attribute targeting {FormatType(target)} {where}; the first registered rule is used.");
+ continue;
+ }
+ registry.Add(target, new AssemblyLayoutEntry(attr, assembly.GetName().Name));
+ }
+ }
+
+ s_AssemblyLayoutRegistry = registry;
+ return s_AssemblyLayoutRegistry;
+ }
+
+ // Gates which [DictionaryDisplayForType] rules an assembly may declare: you may only style a
+ // Dictionary that involves a type you authored *anywhere in its shape*, not merely as the
+ // direct K or V. A closed generic's own Assembly is its definition's (Dictionary<,> and List<>
+ // live in the framework), so ownership is carried by the type arguments, and we recurse through
+ // element types and nested generic arguments to find an authored type at any depth. This is
+ // intentionally broader than "the direct key or value is owned", because a dictionary that
+ // contains your type is legitimately yours to style. For an assembly that defines only MyType:
+ // Dictionary -> owned (MyType is the direct value)
+ // Dictionary -> owned (an array reports its element's assembly)
+ // Dictionary> -> owned (MyType nested in the value's generic args)
+ // Dictionary> -> owned (MyType nested in the inner dictionary)
+ // Dictionary -> NOT owned (no authored type appears anywhere)
+ // The gate's purpose is only to stop an assembly from hijacking a shape it has no stake in (the
+ // last case); it is not meant to require ownership of the outermost key/value specifically.
+ static bool DeclaresTargetType(Type type, Assembly declaringAssembly)
+ {
+ if (type == null)
+ return false;
+ if (type.Assembly == declaringAssembly)
+ return true;
+ if (type.HasElementType && DeclaresTargetType(type.GetElementType(), declaringAssembly))
+ return true;
+ if (type.IsGenericType)
+ {
+ foreach (var arg in type.GetGenericArguments())
+ {
+ if (DeclaresTargetType(arg, declaringAssembly))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Builds the warning for a Dictionary rule rejected by the ownership gate. It names K and V
+ // and states the rule in plain English: the fix is for the developer to declare the attribute in
+ // whichever assembly defines a type used in the dictionary — they know which one that is, so we
+ // don't try to guess it. A rejection means no authored type appears anywhere in the shape (not the
+ // direct key or value, nor any type nested within them), so naming the top-level key and value is
+ // enough to point at the problem. target is a closed Dictionary<,> (guaranteed by IsExactDictionaryType upstream).
+ static string OwnershipRejectionMessage(Type target, Assembly declaringAssembly)
+ {
+ var args = target.GetGenericArguments();
+ var declaringName = declaringAssembly.GetName().Name;
+
+ return $"The DictionaryDisplayForType attribute targeting {FormatType(target)} in {declaringName} is ignored: it must "
+ + $"target a dictionary whose key, value, or a type nested within either is defined in the declaring assembly, "
+ + $"but neither key '{FormatType(args[0])}' nor value '{FormatType(args[1])}' involves a type defined in {declaringName}. "
+ + $"Apply the attribute in the assembly that defines a key or value type used by the dictionary.";
+ }
+
+ // Renders a Type for a warning: angle-bracket generics (Dictionary`2[Int32,Foo] ->
+ // Dictionary), recursing through generic arguments. Arrays (and other
+ // element types) are not generic, so format the element type and re-append the suffix
+ // (e.g. List`1[] -> List[]) rather than printing the raw runtime name.
+ static string FormatType(Type type)
+ {
+ if (type == null)
+ return "";
+ if (type.HasElementType)
+ {
+ var element = type.GetElementType();
+ var runtimeName = type.Name;
+ var suffix = runtimeName.StartsWith(element.Name) ? runtimeName.Substring(element.Name.Length) : string.Empty;
+ return FormatType(element) + suffix;
+ }
+ if (!type.IsGenericType)
+ return type.Name;
+
+ var name = type.Name;
+ var tick = name.IndexOf('`');
+ if (tick >= 0)
+ name = name.Substring(0, tick);
+ var args = Array.ConvertAll(type.GetGenericArguments(), FormatType);
+ return $"{name}<{string.Join(", ", args)}>";
}
internal static bool IsEditingMultipleObjects(SerializedProperty property)
@@ -445,24 +828,40 @@ internal static int InsertOrDuplicateSelectedEntry(
return lastIndex;
}
- internal static int FindFirstDuplicateDisplayIndex(
+ internal static int FindFirstIgnoredDisplayIndex(
IEnumerable duplicateArrayIndices,
+ IEnumerable nullKeyArrayIndices,
SortedIndexMap sortedIndices)
{
- if (duplicateArrayIndices == null)
- return -1;
-
int firstDisplayIndex = int.MaxValue;
- foreach (var arrayIndex in duplicateArrayIndices)
+ firstDisplayIndex = MinDisplayIndex(duplicateArrayIndices, sortedIndices, firstDisplayIndex);
+ firstDisplayIndex = MinDisplayIndex(nullKeyArrayIndices, sortedIndices, firstDisplayIndex);
+ return firstDisplayIndex == int.MaxValue ? -1 : firstDisplayIndex;
+ }
+
+ static int MinDisplayIndex(IEnumerable arrayIndices, SortedIndexMap sortedIndices, int current)
+ {
+ if (arrayIndices == null)
+ return current;
+
+ foreach (var arrayIndex in arrayIndices)
{
if (!sortedIndices.ContainsArrayIndex(arrayIndex))
continue;
int displayIndex = sortedIndices.ToDisplayIndex(arrayIndex);
- if (displayIndex < firstDisplayIndex)
- firstDisplayIndex = displayIndex;
+ if (displayIndex < current)
+ current = displayIndex;
}
+ return current;
+ }
- return firstDisplayIndex == int.MaxValue ? -1 : firstDisplayIndex;
+ internal static bool RemoveEntryAtDisplayIndex(SerializedProperty arrayProperty, int index, SortedIndexMap sortedIndices)
+ {
+ using (ListPool.Get(out var tempList))
+ {
+ tempList.Add(index);
+ return RemoveEntriesAtDisplayIndices(arrayProperty, tempList, sortedIndices);
+ }
}
// Performs the dictionary "Remove" mutation: maps the current selection from
@@ -520,11 +919,26 @@ public enum KeyMarkerKind
{
None,
Duplicate,
+ NullKey,
}
- public static KeyMarkerKind GetMarkerKind(int arrayIndex, HashSet duplicateEntryIndices)
+ public static KeyMarkerKind GetMarkerKind(int arrayIndex, HashSet duplicateEntryIndices, HashSet nullKeyEntryIndices)
{
- return duplicateEntryIndices.Contains(arrayIndex) ? KeyMarkerKind.Duplicate : KeyMarkerKind.None;
+ if (duplicateEntryIndices != null && duplicateEntryIndices.Contains(arrayIndex))
+ return KeyMarkerKind.Duplicate;
+ if (nullKeyEntryIndices != null && nullKeyEntryIndices.Contains(arrayIndex))
+ return KeyMarkerKind.NullKey;
+ return KeyMarkerKind.None;
+ }
+
+ public static string GetMarkerTooltip(KeyMarkerKind kind)
+ {
+ switch (kind)
+ {
+ case KeyMarkerKind.Duplicate: return DictionaryDrawer.Texts.DuplicateMarkerTooltip;
+ case KeyMarkerKind.NullKey: return DictionaryDrawer.Texts.NullKeyMarkerTooltip;
+ default: return null;
+ }
}
}
diff --git a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerUITK.cs b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerUITK.cs
index 384c0b1df3..a9270caa74 100644
--- a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerUITK.cs
+++ b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryDrawerUITK.cs
@@ -16,6 +16,12 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var dictionaryView = new DictionaryView();
+ // preferredLabel is set by PropertyField just before this call (label ?? localizedDisplayName).
+ // The view uses it as its foldout title, so a nested dictionary value — which the enclosing
+ // drawer labels "Dictionary" via GetNestedCollectionValueLabel — reads correctly without the
+ // view having to detect that case itself.
+ dictionaryView.preferredLabel = preferredLabel;
+
// At this point the view has just been constructed and is not yet
// parented, so dictionaryView.panel is null. Calling BindProperty here
// would (a) set bindingPath, and (b) take the "element.panel == null"
diff --git a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryView.cs b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryView.cs
index 3d1f735d31..1f9ff83e40 100644
--- a/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryView.cs
+++ b/Editor/Mono/GUI/PropertyDrawers/Dictionary/DictionaryView.cs
@@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
-using System.Text.RegularExpressions;
using Unity.Profiling;
using Unity.Profiling.LowLevel;
using UnityEditor.UIElements;
using UnityEngine.UIElements.Internal;
using UnityEditor;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEngine.UIElements
{
@@ -19,7 +19,7 @@ namespace UnityEngine.UIElements
/// field. Owns the foldout header
/// (via ), the +/- footer
/// (via ), the two-column
-/// "Key | Value" header with its draggable resizer, sort + duplicate
+/// "Key | Value" header with its draggable resizer, sort + ignored-key
/// detection state, and all per-property data needed to render and mutate
/// the dictionary.
///
@@ -36,7 +36,7 @@ namespace UnityEngine.UIElements
/// BindingRequest that re-walks the tree on the next panel update
/// and re-dispatches the bind event, running the rebuild path twice.
///
-internal class DictionaryView : ListView
+internal partial class DictionaryView : ListView
{
// All USS classes added by the dictionary view live under the `unity-dictionary-view`
// block so they're easy to distinguish from classes inherited from BaseListView,
@@ -56,24 +56,59 @@ internal class DictionaryView : ListView
static readonly string k_ListViewClass = ussClassName + "__list-view";
static readonly string k_ListViewFocusedClass = ussClassName + "__list-view--focused";
static readonly string k_ListHeaderClass = ussClassName + "__list-header";
+ // Leak-safe styling hook for the slim one-column header (see k_OneColumnModeClass for why
+ // header/row styling can't key on the view root). The header subtree never contains a nested
+ // dictionary view, so its rules may descend from this marker freely.
+ static readonly string k_ListHeaderOneColumnClass = k_ListHeaderClass + "--one-column";
static readonly string k_ListHeaderKeyClass = ussClassName + "__list-header__key";
static readonly string k_ListHeaderValueClass = ussClassName + "__list-header__value";
static readonly string k_RowClass = ussClassName + "__row";
+ // Leak-safe styling hook for one-column rows/cells (see k_OneColumnModeClass). Row and cell
+ // rules match via the child combinator from this (e.g. __row--one-column > __field--key) so
+ // they reach only this view's own rows, never a nested dictionary's rows deeper down.
+ static readonly string k_RowOneColumnClass = k_RowClass + "--one-column";
static readonly string k_HelpBoxClass = ussClassName + "__helpbox";
- static readonly string k_HelpBoxDuplicatesClass = ussClassName + "__helpbox--duplicates";
- static readonly string k_HelpBoxSelectDuplicateClass = ussClassName + "__helpbox__select-duplicate";
+ static readonly string k_HelpBoxIgnoredClass = ussClassName + "__helpbox--duplicates";
+ static readonly string k_HelpBoxSelectIgnoredClass = ussClassName + "__helpbox__select-duplicate";
static readonly string k_HeaderSpacerClass = ussClassName + "__header-spacer";
static readonly string k_ToggleLabelClass = ussClassName + "__toggle-label";
static readonly string k_EmptyLabelClass = ussClassName + "__empty-label";
static readonly string k_HeaderInfoClass = ussClassName + "__header-info";
- static readonly string k_DuplicateKeyIconClass = ussClassName + "__duplicate-key-icon";
+ static readonly string k_KeyWarningIconClass = ussClassName + "__duplicate-key-icon";
static readonly string k_SelectionIndicatorClass = ussClassName + "__selection-indicator";
static readonly string k_ColumnResizerClass = ussClassName + "__column-resizer";
static readonly string k_ColumnResizerLineClass = ussClassName + "__column-resizer__line";
+ // View-level source of truth for one-column mode, toggled on the ListView root. It carries
+ // NO styling on purpose: a rule descending from it (.unity-dictionary-view--one-column X)
+ // would also match a NESTED dictionary's elements, since a nested dict lives inside an outer
+ // one-column row — that descendant leak is exactly what the per-element markers above
+ // (k_RowOneColumnClass, k_ListHeaderOneColumnClass) avoid. We keep this root marker because
+ // it's the stable, content-independent signal for "is this view one-column?": present even
+ // when the view has no rows (the per-element markers only exist alongside their elements),
+ // so tests/debugging assert against it rather than fishing for an internal modifier.
+ static readonly string k_OneColumnModeClass = ussClassName + "--one-column";
+ // Per-row "Value" disclosure foldout shown in the OneColumnWithValueFoldout layout. Its
+ // contentContainer holds the value field, so the foldout drives native show/hide + content
+ // indentation.
+ static readonly string k_RowFoldoutClass = ussClassName + "__row-foldout";
+ // Static bold "Value" header shown in the OneColumnWithValueVisible layout in place of a
+ // foldout, so the value is always visible and the header is plain, non-interactive text.
+ static readonly string k_RowValueHeaderClass = ussClassName + "__row-value-header";
+
+ // Holds the live DictionaryView siblings that share one persisted DictionaryState — the elements of a
+ // List/array/Dictionary of dictionaries, whose paths all normalize to the same key. Because
+ // they share a single StateCache entry (avoiding an explosion of per-instance caches), a
+ // sort-direction, layout, or column-width change made in one sibling must propagate live to
+ // the others.
+ // Key is the same Hash128 as s_StateCache (the normalized property path)
+ [AutoStaticsCleanupOnCodeReload]
+ static readonly Dictionary> s_LinkedViews = new();
SerializedProperty m_DictionaryFieldProperty;
SerializedProperty m_ArrayProperty;
Hash128 m_StateCacheKey;
+ bool m_ShouldLinkWithSiblings;
+ bool m_IsLinked;
readonly Foldout m_Foldout;
Label m_HeaderInfoLabel;
@@ -81,16 +116,42 @@ internal class DictionaryView : ListView
VisualElement m_ListHeader;
ColumnResizer m_ColumnResizer;
VisualElement m_KeyHeader;
+ Label m_KeyHeaderLabel;
Label m_ValueLabel;
VisualElement m_SortIndicator;
+ // Resolved key column label (default "Key" or DictionaryDisplayAttribute override).
+ string m_KeyLabelText;
+ // Resolved value column label (default "Value" or DictionaryDisplayAttribute override),
+ // reused as the per-row foldout text in one-column mode.
+ string m_ValueLabelText;
+ // Label for the value cell's PropertyField: "Array"/"List" when the value type is a collection
+ // (so the built-in collection drawer's header reads that instead of falling back to "Value"),
+ // otherwise string.Empty. Uniform across rows since every entry shares the value type.
+ string m_ValueFieldLabel = string.Empty;
+ // Foldout title supplied by the hosting PropertyField (PropertyDrawer.preferredLabel) and set in
+ // DictionaryDrawer.CreatePropertyGUI. A nested dictionary value arrives labelled "Dictionary" from
+ // its enclosing drawer; an empty/null value falls back to the property's own localizedDisplayName,
+ // mirroring how ListView resolves its header title.
+ internal string preferredLabel { get; set; }
HelpBox m_MultiEditHelpBox;
- HelpBox m_DuplicatesHelpBox;
+ HelpBox m_IgnoredHelpBox;
readonly HashSet m_DuplicateEntryIndices = new();
+ readonly HashSet m_NullKeyEntryIndices = new();
bool m_SortScheduled;
bool m_SortAscending = true;
+ // Single source of truth for the column layout. Persisted in DictionaryState and
+ // shared across list siblings via the normalized path. The two booleans below are
+ // derived views kept so the render code reads intent-named flags rather than enum
+ // comparisons. The two OneColumn_* modes both stack Key over Value; they differ
+ // only in whether each value sits behind a per-row "Value" foldout.
+ DictionaryLayout m_Layout = DictionaryLayout.TwoColumns;
+ // Default layout resolved from [DictionaryDisplay] (field- or assembly-level); the
+ // active layout falls back to this until the user overrides it from the context menu.
+ DictionaryLayout m_AttributeLayout = DictionaryLayout.TwoColumns;
+ bool m_OneColumnMode => m_Layout != DictionaryLayout.TwoColumns;
DictionaryDrawer.SortedIndexMap m_SortedIndexMap = DictionaryDrawer.SortedIndexMap.Empty;
// Hash of the keys at the time we last produced m_SortedIndexMap.
// Lets the TrackPropertyValue callback skip sort scheduling when only a
@@ -103,6 +164,11 @@ internal class DictionaryView : ListView
bool m_IsBound;
+ // The layout the installed makeItem factory was built for. Each layout uses a distinct row
+ // template (two-column, one-column foldout, one-column static value), so a layout change
+ // compares against this to know it must rebuild the row pool with the new template.
+ DictionaryLayout m_MakeItemLayout = DictionaryLayout.TwoColumns;
+
int displayedItemCount => m_SortedIndexMap.Length;
public Foldout foldout => m_Foldout;
@@ -152,13 +218,81 @@ public DictionaryView()
autoSelectNewItemOnAdd = false;
makeNoneElement = MakeEmptyElement;
- makeItem = MakeListItem;
+ // Default to the two-column template; RebuildFromProperty/ApplyLayoutMode swap in
+ // the one-column factory when the persisted/selected layout calls for it.
+ makeItem = MakeTwoColumnRow;
bindItem = BindListItem;
unbindItem = UnbindListItem;
destroyItem = DestroyListItem;
onAdd = _ => OnAddClicked();
onRemove = _ => OnRemoveClicked();
- selectionChanged += OnSelectionChanged;
+
+ RegisterCallback(_ => OnViewAttachToPanel());
+ RegisterCallback(_ => OnViewDetachFromPanel());
+ }
+
+ void OnViewAttachToPanel()
+ {
+ DictionaryDrawer.SerializedOrderChanged += OnSerializedOrderChanged;
+ AddLinkedViewIfNeeded();
+ }
+
+ void OnViewDetachFromPanel()
+ {
+ DictionaryDrawer.SerializedOrderChanged -= OnSerializedOrderChanged;
+ RemoveLinkedViewIfNeeded();
+ }
+
+ void AddLinkedViewIfNeeded()
+ {
+ if (m_IsLinked || m_ArrayProperty == null || panel == null)
+ return;
+ if (!m_ShouldLinkWithSiblings)
+ return;
+
+ if (!s_LinkedViews.TryGetValue(m_StateCacheKey, out var list))
+ {
+ list = new List();
+ s_LinkedViews[m_StateCacheKey] = list;
+ }
+ list.Add(this);
+ m_IsLinked = true;
+ }
+
+ void RemoveLinkedViewIfNeeded()
+ {
+ if (!m_IsLinked)
+ return;
+
+ if (s_LinkedViews.TryGetValue(m_StateCacheKey, out var list))
+ {
+ list.Remove(this);
+ if (list.Count == 0)
+ s_LinkedViews.Remove(m_StateCacheKey);
+ }
+ m_IsLinked = false;
+ }
+
+ void PerformActionOnLinkedViews(Action action)
+ {
+ if (!m_IsLinked || !s_LinkedViews.TryGetValue(m_StateCacheKey, out var siblings) || siblings.Count <= 1)
+ return;
+
+ foreach (var view in siblings)
+ {
+ if (view == this)
+ continue;
+ action(view);
+ }
+ }
+
+ void OnSerializedOrderChanged()
+ {
+ if (!m_IsBound || m_ArrayProperty == null)
+ return;
+ UpdateSortIndicatorClass();
+ RebuildSortedIndicesAndRefresh();
+ UpdateHeaderInfo();
}
[EventInterest(typeof(SerializedPropertyBindEvent))]
@@ -174,14 +308,14 @@ protected override void HandleEventBubbleUp(EventBase evt)
}
// Tears down any prior bound state and rebuilds the view against the new
- // property. Resolves key/value types and the optional DictionaryHeaderAttribute
+ // property. Resolves key/value types and the optional DictionaryDisplayAttribute
// from the property's reflected FieldInfo, builds the column header, sort
- // scheduler, and duplicate tracking, and installs makeItem/bindItem/onAdd/onRemove
+ // scheduler, and ignored-key tracking, and installs makeItem/bindItem/onAdd/onRemove
// on the list. Only called from HandleEventBubbleUp's SerializedPropertyBindEvent
// handler; external callers bind via bindingPath + the inspector's tree walk.
void RebuildFromProperty(SerializedProperty property)
{
- using var _ = s_BuildMarker.Auto();
+ using var buildMarker = s_BuildMarker.Auto();
if (property == null)
return;
@@ -189,19 +323,39 @@ void RebuildFromProperty(SerializedProperty property)
ResetBoundState();
m_DictionaryFieldProperty = property.Copy();
- m_StateCacheKey = DictionaryDrawer.ComputeStateCacheKey(m_DictionaryFieldProperty.propertyPath);
+ var dictionaryPropertyPath = m_DictionaryFieldProperty.propertyPath;
+ m_StateCacheKey = DictionaryDrawer.ComputeStateCacheKey(dictionaryPropertyPath);
+ m_ShouldLinkWithSiblings = DictionaryDrawer.ShouldLinkViewStateWithSiblings(dictionaryPropertyPath);
+
+ var fieldInfo = ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromProperty(m_DictionaryFieldProperty, out var dictionaryType);
+ m_AttributeLayout = DictionaryDrawer.ResolveDefaultLayout(fieldInfo, dictionaryType);
+
+ // Resolve the value-cell label before SyncMakeItemToLayout()/RefreshListView() build any rows,
+ // so the value PropertyField is created with the right collection label on first paint.
+ var valueType = dictionaryType != null && dictionaryType.IsGenericType
+ ? dictionaryType.GetGenericArguments()[1]
+ : null;
+ m_ValueFieldLabel = DictionaryDrawer.GetNestedCollectionValueLabel(valueType) ?? string.Empty;
var cachedState = DictionaryDrawer.GetCachedState(m_StateCacheKey);
if (cachedState != null)
- {
m_SortAscending = cachedState.sortAscending;
- }
+ m_Layout = DictionaryDrawer.GetActiveLayout(m_StateCacheKey, m_AttributeLayout);
+
+ // Install the matching row template before RefreshListView() builds any rows, so
+ // the persisted layout is reflected on first paint without a redundant rebuild.
+ SyncMakeItemToLayout();
// Foldout's bindingPath drives open/closed state via SerializedProperty.isExpanded,
- // so the user's collapse/expand survives rebuilds and domain reloads.
- m_Foldout.text = m_DictionaryFieldProperty.displayName;
- m_Foldout.bindingPath = m_DictionaryFieldProperty.propertyPath;
- headerTitle = m_DictionaryFieldProperty.displayName;
+ // so the user's collapse/expand survives rebuilds and domain reloads. The title follows the
+ // label the hosting PropertyField supplied (like ListView.headerTitle does), so a nested
+ // dictionary value reads "Dictionary" while a real field keeps its own display name.
+ var foldoutLabel = string.IsNullOrEmpty(preferredLabel)
+ ? m_DictionaryFieldProperty.localizedDisplayName
+ : preferredLabel;
+ m_Foldout.text = foldoutLabel;
+ m_Foldout.bindingPath = dictionaryPropertyPath;
+ headerTitle = foldoutLabel;
if (DictionaryDrawer.IsEditingMultipleObjects(m_DictionaryFieldProperty))
{
@@ -218,7 +372,7 @@ void RebuildFromProperty(SerializedProperty property)
arrayProp.Next(true);
m_ArrayProperty = arrayProp;
- BuildDuplicatesHelpBox(m_Foldout.contentContainer);
+ BuildIgnoredHelpBox(m_Foldout.contentContainer);
BuildFoldoutHeaderInfoLabel();
SetTwoColumnHeader(BuildColumnHeader());
@@ -233,9 +387,16 @@ void RebuildFromProperty(SerializedProperty property)
RebuildSortedIndices();
RefreshListView();
UpdateHeaderInfo();
- UpdateRemoveButtonState();
+
+ // Apply the restored persisted layout mode after the header + list are built
+ // so the root modifier class and per-row config reflect it on first paint.
+ // RefreshListView() above already bound the rows with the correct layout, so
+ // skip the redundant rebind here.
+ ApplyLayoutMode(refresh: false);
m_IsBound = true;
+
+ AddLinkedViewIfNeeded();
}
void ResetBoundState()
@@ -243,6 +404,8 @@ void ResetBoundState()
if (!m_IsBound)
return;
+ RemoveLinkedViewIfNeeded();
+
// Tear down everything RebuildFromProperty installs so a rebuild for a
// different property starts from a clean slate. The foldout itself stays
// since its bindingPath gets reassigned below.
@@ -257,10 +420,10 @@ void ResetBoundState()
var listFooter = m_Foldout.Q(className: BaseListView.footerUssClassName);
if (listFooter != null)
listFooter.style.display = DisplayStyle.Flex;
- if (m_DuplicatesHelpBox != null)
+ if (m_IgnoredHelpBox != null)
{
- m_DuplicatesHelpBox.RemoveFromHierarchy();
- m_DuplicatesHelpBox = null;
+ m_IgnoredHelpBox.RemoveFromHierarchy();
+ m_IgnoredHelpBox = null;
}
if (m_ListHeader != null)
{
@@ -280,7 +443,6 @@ void ResetBoundState()
makeNoneElement = null;
onAdd = null;
onRemove = null;
- selectionChanged -= OnSelectionChanged;
UnregisterCallback(OnValidateCommand);
UnregisterCallback(OnExecuteCommand);
UnregisterCallback(OnDictionaryKeyDown);
@@ -292,8 +454,10 @@ void ResetBoundState()
m_DictionaryFieldProperty = null;
m_ArrayProperty = null;
+ m_ShouldLinkWithSiblings = false;
m_DuplicateEntryIndices.Clear();
+ m_NullKeyEntryIndices.Clear();
m_ItemsSource = null;
itemsSource = null;
m_SortedIndexMap = DictionaryDrawer.SortedIndexMap.Empty;
@@ -303,6 +467,12 @@ void ResetBoundState()
m_KeyHeader = null;
m_ValueLabel = null;
m_SortIndicator = null;
+ // Reset the layout and its root modifier so a rebind to a different (or
+ // multi-edit) property starts from the default two-column layout rather than
+ // leaking the previous view's one-column class.
+ m_Layout = DictionaryLayout.TwoColumns;
+ m_AttributeLayout = DictionaryLayout.TwoColumns;
+ EnableInClassList(k_OneColumnModeClass, false);
m_IsBound = false;
}
@@ -329,11 +499,6 @@ void ShowMultiEditHelpBox()
listFooter.style.display = DisplayStyle.None;
}
- void OnSelectionChanged(IEnumerable _)
- {
- UpdateRemoveButtonState();
- }
-
void OnTrackedPropertyChanged(object _, SerializedProperty __)
{
if (m_ArrayProperty == null || !m_ArrayProperty.isValid)
@@ -355,13 +520,13 @@ void OnTrackedPropertyChanged(object _, SerializedProperty __)
CheckIfKeysChangedAndSortIfNeeded();
// While a key is being edited (or any other editor interaction is in
- // flight) the pending sort can't run yet — keep duplicate markers in
+ // flight) the pending sort can't run yet — keep the key warning markers in
// sync so the user sees live feedback as they type. Sorting itself
// would yank the focused field out of their hands.
- if (!IsReadyToSortByKey() && UpdateDuplicateIndicesOnly())
+ if (!IsReadyToSortByKey() && UpdateMarkerIndicesOnly())
{
UpdateHeaderInfo();
- UpdateDuplicateKeyIconsOnVisibleItems();
+ UpdateKeyWarningIconsOnVisibleItems();
}
}
@@ -385,28 +550,28 @@ void BuildFoldoutHeaderInfoLabel()
toggle.Add(m_HeaderInfoLabel);
}
- void BuildDuplicatesHelpBox(VisualElement parent)
+ void BuildIgnoredHelpBox(VisualElement parent)
{
- // The duplicates helpbox starts hidden via .unity-dictionary-view__helpbox--duplicates
+ // The ignored-rows helpbox starts hidden via .unity-dictionary-view__helpbox--duplicates
// (display: none in USS); UpdateHeaderInfo flips style.display when the
- // duplicate count goes non-zero.
- m_DuplicatesHelpBox = new HelpBox(string.Empty, HelpBoxMessageType.Warning);
- m_DuplicatesHelpBox.AddToClassList(k_HelpBoxClass);
- m_DuplicatesHelpBox.AddToClassList(k_HelpBoxDuplicatesClass);
+ // ignored count (duplicate + null keys) goes non-zero.
+ m_IgnoredHelpBox = new HelpBox(string.Empty, HelpBoxMessageType.Warning);
+ m_IgnoredHelpBox.AddToClassList(k_HelpBoxClass);
+ m_IgnoredHelpBox.AddToClassList(k_HelpBoxIgnoredClass);
- var selectButton = new Button(OnSelectFirstDuplicateClicked)
+ var selectButton = new Button(OnSelectFirstIgnoredClicked)
{
- text = DictionaryDrawer.Texts.SelectFirstDuplicateButtonLabel
+ text = DictionaryDrawer.Texts.SelectFirstIgnoredButtonLabel
};
- selectButton.AddToClassList(k_HelpBoxSelectDuplicateClass);
- m_DuplicatesHelpBox.Add(selectButton);
+ selectButton.AddToClassList(k_HelpBoxSelectIgnoredClass);
+ m_IgnoredHelpBox.Add(selectButton);
- parent.Add(m_DuplicatesHelpBox);
+ parent.Add(m_IgnoredHelpBox);
}
- void OnSelectFirstDuplicateClicked()
+ void OnSelectFirstIgnoredClicked()
{
- int firstDisplayIndex = DictionaryDrawer.FindFirstDuplicateDisplayIndex(m_DuplicateEntryIndices, m_SortedIndexMap);
+ int firstDisplayIndex = DictionaryDrawer.FindFirstIgnoredDisplayIndex(m_DuplicateEntryIndices, m_NullKeyEntryIndices, m_SortedIndexMap);
if (firstDisplayIndex < 0)
return;
@@ -417,8 +582,10 @@ void OnSelectFirstDuplicateClicked()
VisualElement BuildColumnHeader()
{
- var fieldInfo = ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromProperty(m_DictionaryFieldProperty, out _);
- DictionaryDrawer.GetHeaderLabels(fieldInfo, out var keyLabelText, out var valueLabelText, out var attributeFraction);
+ var fieldInfo = ScriptAttributeUtility.GetFieldInfoAndStaticTypeFromProperty(m_DictionaryFieldProperty, out var dictionaryType);
+ DictionaryDrawer.GetHeaderLabels(fieldInfo, dictionaryType, out var keyLabelText, out var valueLabelText, out var attributeFraction);
+ m_KeyLabelText = keyLabelText;
+ m_ValueLabelText = valueLabelText;
var header = new VisualElement();
header.AddToClassList(k_ListHeaderClass);
@@ -431,8 +598,8 @@ VisualElement BuildColumnHeader()
// keyLabel is styled via .unity-dictionary-view__list-header__key > .unity-text-element
// (ellipsis overflow + flex grow/shrink), so no inline styles needed here.
- var keyLabel = new Label(keyLabelText);
- m_KeyHeader.Add(keyLabel);
+ m_KeyHeaderLabel = new Label(keyLabelText);
+ m_KeyHeader.Add(m_KeyHeaderLabel);
m_SortIndicator = new VisualElement();
m_SortIndicator.AddToClassList(MultiColumnHeaderColumnSortIndicator.ussClassName);
@@ -449,7 +616,7 @@ VisualElement BuildColumnHeader()
header.Add(m_KeyHeader);
m_ColumnResizer = new ColumnResizer(
- header, m_DictionaryFieldProperty.propertyPath, attributeFraction, UpdateColumnWidths);
+ header, m_DictionaryFieldProperty.propertyPath, attributeFraction, onDragged: OnColumnResizerDragged);
header.Add(m_ColumnResizer.BuildElement());
header.RegisterCallback(evt =>
{
@@ -459,6 +626,16 @@ VisualElement BuildColumnHeader()
header.Add(m_ValueLabel);
header.AddManipulator(new ContextualMenuManipulator(evt =>
{
+ // The three layouts form a radio group (the active one is checked), followed
+ // by a separator and the "Reset to Defaults" action.
+ AppendLayoutAction(evt.menu, DictionaryDrawer.Texts.TwoColumnsLayoutLabel, DictionaryLayout.TwoColumns);
+ AppendLayoutAction(evt.menu, DictionaryDrawer.Texts.OneColumnWithValueFoldoutLayoutLabel, DictionaryLayout.OneColumnWithValueFoldout);
+ AppendLayoutAction(evt.menu, DictionaryDrawer.Texts.OneColumnWithValueVisibleLayoutLabel, DictionaryLayout.OneColumnWithValueVisible);
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction(DictionaryDrawer.Texts.ShowSerializedOrderLabel,
+ _ => DictionaryDrawer.SetShowSerializedOrder(!DictionaryDrawer.ShowSerializedOrder),
+ _ => DictionaryDrawer.ShowSerializedOrder ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
+ evt.menu.AppendSeparator();
evt.menu.AppendAction(DictionaryDrawer.Texts.ResetToDefaultsLabel,
_ => ResetToDefaults(),
_ => DictionaryDrawer.HasCachedState(m_StateCacheKey) ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
@@ -467,15 +644,41 @@ VisualElement BuildColumnHeader()
return header;
}
- VisualElement MakeListItem()
+ // Strongly-typed list row. Caches its child references so BindListItem reads them
+ // directly instead of re-running Q<>() tree queries on every bind during scroll, and
+ // stores the display index in a typed field (no int->object boxing, which element.userData
+ // would incur). Two-column rows use DictionaryRow; one-column rows use the subclass below.
+ class DictionaryRow : VisualElement
{
- var container = new VisualElement();
- container.AddToClassList("unity-list-view__reorderable-item__container");
- container.AddToClassList(k_RowClass);
- container.name = "dict-element";
+ public int displayIndex = -1;
+ public VisualElement keyContainer;
+ public VisualElement valueContainer;
+ public VisualElement keyWarningIcon;
+ public PropertyField keyField;
+ public PropertyField valueField;
+ }
- var duplicateKeyIcon = new VisualElement();
- duplicateKeyIcon.AddToClassList(k_DuplicateKeyIconClass);
+ // One-column foldout row: the value field lives in valueFoldout.contentContainer (parented
+ // once at build, never reparented per bind), so the foldout owns native show/hide and content
+ // indentation. Only the OneColumnWithValueFoldout layout uses this; the value-visible layout
+ // uses a plain DictionaryRow with a static header (see MakeOneColumnVisibleRow).
+ sealed class OneColumnDictionaryRow : DictionaryRow
+ {
+ public Foldout valueFoldout;
+ }
+
+ // Builds the parts every row shares regardless of layout: the row container, the
+ // key warning icon, the key/value cell containers + their PropertyFields, the
+ // selection indicator, and the row pointer-down handler. The per-mode factory below
+ // decides how the value field is parented under valueContainer.
+ DictionaryRow BuildRowScaffold(DictionaryRow row)
+ {
+ row.AddToClassList("unity-list-view__reorderable-item__container");
+ row.AddToClassList(k_RowClass);
+ row.name = "dict-element";
+
+ var keyWarningIcon = new VisualElement();
+ keyWarningIcon.AddToClassList(k_KeyWarningIconClass);
var keyContainer = new VisualElement { name = "key-container" };
keyContainer.AddToClassList(k_DrawerFieldClass);
@@ -498,59 +701,158 @@ VisualElement MakeListItem()
ApplyKeyColumnWidth(keyContainer);
- // Build the row's two PropertyFields up-front and parent them under the
- // key/value containers. They start unbound — BindListItem just calls
- // BindProperty on them every time the row is reused, hitting the fast
- // rebind path in PropertyField.ResetInternal so the child field tree is
- // not torn down and rebuilt per row.
+ // Build the row's two PropertyFields up-front. They start unbound — BindListItem
+ // just calls BindProperty on them every time the row is reused, hitting the fast
+ // rebind path in PropertyField.ResetInternal so the child field tree is not torn
+ // down and rebuilt per row.
var keyField = new PropertyField(property: null, label: string.Empty, showFirstFoldoutHeader: false);
- var valueField = new PropertyField(property: null, label: string.Empty, showFirstFoldoutHeader: false);
+ // m_ValueFieldLabel is "Array"/"List" for collection value types, else string.Empty; an empty
+ // label lets PropertyField fall back to the value's "Value" displayName (e.g. leaf/struct cells).
+ var valueField = new PropertyField(property: null, label: m_ValueFieldLabel, showFirstFoldoutHeader: false);
+
keyContainer.Add(keyField);
- valueContainer.Add(valueField);
var selectionIndicator = new VisualElement();
selectionIndicator.AddToClassList(k_SelectionIndicatorClass);
selectionIndicator.pickingMode = PickingMode.Ignore;
- container.Add(duplicateKeyIcon);
- container.Add(keyContainer);
- container.Add(valueContainer);
- container.Add(selectionIndicator);
+ row.Add(keyWarningIcon);
+ row.Add(keyContainer);
+ row.Add(valueContainer);
+ row.Add(selectionIndicator);
+
+ row.RegisterCallback(OnRowPointerDown, TrickleDown.TrickleDown);
+ // Select the row when an object is dropped onto its key cell. Registered as
+ // TrickleDown so it runs before the key ObjectField's own DragPerform handler
+ // (which sets the value and stops propagation) — the drop then changes the key,
+ // which re-sorts and moves the row; because the entry is now selected,
+ // SortIfNeeded's RestoreSelectionByArrayIndices keeps it highlighted and scrolls
+ // it into view so it's clear where the entry went.
+ row.RegisterCallback(OnRowDragPerform, TrickleDown.TrickleDown);
+
+ row.keyContainer = keyContainer;
+ row.valueContainer = valueContainer;
+ row.keyWarningIcon = keyWarningIcon;
+ row.keyField = keyField;
+ row.valueField = valueField;
+ return row;
+ }
+
+ // Two-column template: key | value side by side. The value field sits directly in the
+ // value cell; USS + the resizer drive the column widths.
+ VisualElement MakeTwoColumnRow()
+ {
+ var row = BuildRowScaffold(new DictionaryRow());
+ row.valueContainer.Add(row.valueField);
+ return row;
+ }
+
+ // One-column foldout template (OneColumnWithValueFoldout): key stacked over value, with the
+ // value field inside a per-row collapsible "Value" foldout whose content the foldout drives
+ // native show/hide + indentation for. ConfigureOneColumnRow restores each row's expansion
+ // state per bind.
+ VisualElement MakeOneColumnFoldoutRow()
+ {
+ var row = (OneColumnDictionaryRow)BuildRowScaffold(new OneColumnDictionaryRow());
+ row.AddToClassList(k_RowOneColumnClass);
+
+ var foldout = new Foldout { name = "row-foldout" };
+ foldout.AddToClassList(k_RowFoldoutClass);
+ // Foldout.text adds the .unity-foldout__text class (which the bold USS keys on) to
+ // the toggle label; set it once here since the value label is stable per view.
+ foldout.text = m_ValueLabelText;
+ foldout.RegisterValueChangedCallback(evt =>
+ {
+ // Ignore ChangeEvent bubbling up from toggles inside the value field.
+ if (evt.target == foldout)
+ OnRowFoldoutToggled(row, evt.newValue);
+ });
+ foldout.contentContainer.Add(row.valueField);
+
+ row.valueContainer.Add(foldout);
+ row.valueFoldout = foldout;
+ return row;
+ }
+
+ // One-column static template (OneColumnWithValueVisible): key stacked over value, with the
+ // value always shown beneath a plain bold "Value" label. The header is a Label, not a foldout
+ // toggle, so it is inherently non-interactive — clicking it does nothing. Unlike the foldout
+ // variant the value sits directly under the header with no extra indentation.
+ VisualElement MakeOneColumnVisibleRow()
+ {
+ var row = BuildRowScaffold(new DictionaryRow());
+ row.AddToClassList(k_RowOneColumnClass);
- container.RegisterCallback(OnRowPointerDown, TrickleDown.TrickleDown);
+ var valueHeader = new Label(m_ValueLabelText);
+ valueHeader.AddToClassList(k_RowValueHeaderClass);
- return container;
+ row.valueContainer.Add(valueHeader);
+ row.valueContainer.Add(row.valueField);
+ return row;
}
void BindListItem(VisualElement element, int displayIndex)
{
using var _ = s_BindListItemMarker.Auto();
- if (!TryGetArrayIndexForBinding(element, displayIndex, out int arrayIndex))
+ var row = (DictionaryRow)element;
+ if (!TryGetArrayIndexForBinding(row, displayIndex, out int arrayIndex))
return;
- ApplyAlternatingBackground(element, displayIndex);
+ ApplyAlternatingBackground(row, displayIndex);
var arrayElement = m_ArrayProperty.GetArrayElementAtIndex(arrayIndex);
DictionaryDrawer.GetKeyAndValueProperties(arrayElement, out var keyProp, out var valueProp);
- var keyContainer = element.Q("key-container");
- var valueContainer = element.Q("value-container");
- ApplyKeyColumnWidth(keyContainer);
+ ApplyKeyColumnWidth(row.keyContainer);
- // The row's PropertyFields were created in MakeListItem; rebinding flips
+ // The row's PropertyFields were created in the makeItem factory; rebinding flips
// their target property without recreating the child field tree. When the
- // key/value lookup unexpectedly returns null (e.g. corrupted entry), the
- // stale binding is dropped via Unbind() instead of leaving the previous
- // row's data visible.
- RebindCellField(keyContainer, keyProp);
- UpdateDuplicateKeyIconVisibility(element, arrayIndex);
- RebindCellField(valueContainer, valueProp);
+ // key/value lookup unexpectedly returns null (e.g. corrupted entry), the stale
+ // binding is dropped via Unbind() instead of leaving the previous row's data visible.
+ RebindCellField(row.keyField, keyProp);
+ UpdateKeyWarningIconVisibility(row, arrayIndex);
+ RebindCellField(row.valueField, valueProp);
+ // Reassert the collection label on reused rows: the label persists across rebinds, but a
+ // pooled row may have been built for a different value type (the makeItem delegate compares
+ // equal, so the pool isn't rebuilt on rebind). The setter no-ops when already correct.
+ row.valueField.label = m_ValueFieldLabel;
+
+ if (row is OneColumnDictionaryRow oneColumnRow)
+ ConfigureOneColumnRow(oneColumnRow, valueProp);
}
- static void RebindCellField(VisualElement container, SerializedProperty property)
+ // Restores a one-column foldout row's expansion state on bind. The value field's visibility
+ // and indentation are owned by the foldout natively; this just syncs the foldout to the
+ // persisted per-row expansion state. Only called for OneColumnWithValueFoldout rows — the
+ // value-visible layout uses a static, always-shown header with no per-bind state.
+ void ConfigureOneColumnRow(OneColumnDictionaryRow row, SerializedProperty valueProp)
+ {
+ bool expanded = valueProp != null && valueProp.isExpanded;
+ row.valueFoldout.SetValueWithoutNotify(expanded);
+ }
+
+ // Foldout toggle handler for one-column rows: persists the new expansion state on the
+ // value property (so it survives rebuilds/domain reloads). The foldout shows/hides its
+ // content (the value field) natively; DynamicHeight virtualization re-measures the row
+ // on the resulting layout change.
+ void OnRowFoldoutToggled(OneColumnDictionaryRow row, bool expanded)
+ {
+ if (m_ArrayProperty == null || !m_ArrayProperty.isValid)
+ return;
+ int displayIndex = row.displayIndex;
+ if (displayIndex < 0 || displayIndex >= displayedItemCount)
+ return;
+
+ int arrayIndex = DisplayToArrayIndex(displayIndex);
+ var arrayElement = m_ArrayProperty.GetArrayElementAtIndex(arrayIndex);
+ DictionaryDrawer.GetKeyAndValueProperties(arrayElement, out _, out var valueProp);
+ if (valueProp != null)
+ valueProp.isExpanded = expanded;
+ }
+
+ static void RebindCellField(PropertyField field, SerializedProperty property)
{
- var field = container.Q();
if (field == null)
return;
if (property != null)
@@ -567,14 +869,9 @@ void UnbindListItem(VisualElement element, int displayIndex)
static void UnbindRowPropertyFields(VisualElement element)
{
- UnbindCellField(element.Q("key-container"));
- UnbindCellField(element.Q("value-container"));
- }
-
- static void UnbindCellField(VisualElement container)
- {
- var field = container?.Q();
- field?.Unbind();
+ var row = (DictionaryRow)element;
+ row.keyField?.Unbind();
+ row.valueField?.Unbind();
}
static void DestroyListItem(VisualElement element)
@@ -582,13 +879,13 @@ static void DestroyListItem(VisualElement element)
UnbindRowPropertyFields(element);
}
- bool TryGetArrayIndexForBinding(VisualElement element, int displayIndex, out int arrayIndex)
+ bool TryGetArrayIndexForBinding(DictionaryRow row, int displayIndex, out int arrayIndex)
{
arrayIndex = -1;
if (displayIndex < 0 || displayIndex >= displayedItemCount)
return false;
- element.userData = displayIndex;
+ row.displayIndex = displayIndex;
arrayIndex = DisplayToArrayIndex(displayIndex);
return true;
}
@@ -599,6 +896,16 @@ bool TryGetArrayIndexForBinding(VisualElement element, int displayIndex, out int
// alignment — is defined statically in UnityEngine.UIElements.uss.
void ApplyKeyColumnWidth(VisualElement keyContainer)
{
+ // In one-column mode the key spans the full row width (driven by USS on
+ // the --one-column modifier); pushing a pixel width here would fight that,
+ // and the geometry-change loop must not re-stamp stale widths onto rows.
+ if (m_OneColumnMode)
+ {
+ keyContainer.style.width = StyleKeyword.Null;
+ keyContainer.style.flexBasis = StyleKeyword.Null;
+ return;
+ }
+
float keyWidth = GetHeaderSplitLineX();
keyContainer.style.flexBasis = keyWidth;
keyContainer.style.width = keyWidth;
@@ -676,8 +983,9 @@ void FocusContentContainer()
void OnRowPointerDown(PointerDownEvent evt)
{
- if (!(evt.currentTarget is VisualElement row) || !(row.userData is int displayIndex))
+ if (!(evt.currentTarget is DictionaryRow row) || row.displayIndex < 0)
return;
+ int displayIndex = row.displayIndex;
var selectionSnapshot = new HashSet(SelectedDisplayIndices);
schedule.Execute(() =>
@@ -687,12 +995,42 @@ void OnRowPointerDown(PointerDownEvent evt)
});
}
+ // Selects the entry a drag-and-drop is landing on, but only when the drop targets the
+ // key cell — a key change is what re-sorts and relocates the row, so selecting it lets
+ // the deferred SortIfNeeded restore the selection to (and scroll to) the entry's new
+ // position. Value-cell drops don't re-sort, so we leave them to the field alone. This
+ // runs during trickle-down, before the key ObjectField's own handler sets the value and
+ // stops propagation, so the selection is in place before the resulting sort is scheduled.
+ void OnRowDragPerform(DragPerformEvent evt)
+ {
+ if (!(evt.currentTarget is DictionaryRow row) || row.displayIndex < 0)
+ return;
+
+ if (!(evt.target is VisualElement target) || !IsInSubtree(target, row.keyContainer))
+ return;
+
+ SetSelection(row.displayIndex);
+ }
+
+ static bool IsInSubtree(VisualElement element, VisualElement ancestor)
+ {
+ if (element == null || ancestor == null)
+ return false;
+ return element == ancestor || element.FindCommonAncestor(ancestor) == ancestor;
+ }
+
// Header columns mirror what ApplyKeyColumnWidth does for row cells: only
// the resizer-driven width is dynamic, so we just push it onto flex-basis
// and width on both header columns. All other layout (flex grow/shrink,
// min-width, padding-left/right around the resizer) lives in USS.
void ApplyHeaderColumnLayout()
{
+ // One-column mode owns the header layout via ApplyHeaderLayoutMode (key
+ // header spans full width, value label + resizer hidden); skip the
+ // two-column width math so geometry-change callbacks don't fight it.
+ if (m_OneColumnMode)
+ return;
+
if (m_ColumnResizer == null || m_KeyHeader == null || m_ValueLabel == null || m_ListHeader == null)
return;
@@ -723,14 +1061,41 @@ void UpdateColumnWidths()
if (scrollView?.contentContainer == null)
return;
- foreach (var row in scrollView.contentContainer.Children())
+ foreach (var wrapper in scrollView.contentContainer.Children())
+ {
+ if (wrapper.Q() is { } row)
+ ApplyKeyColumnWidth(row.keyContainer);
+ }
+ }
+
+ void OnColumnResizerDragged()
+ {
+ if (m_ColumnResizer == null)
+ return;
+ UpdateColumnWidths();
+ PropagateColumnFractionToLinkedViews(m_ColumnResizer.KeyColumnFraction);
+ }
+
+ void PropagateColumnFractionToLinkedViews(float fraction)
+ {
+ if (!m_IsLinked || !s_LinkedViews.TryGetValue(m_StateCacheKey, out var list) || list.Count <= 1)
+ return;
+
+ foreach (var view in list)
{
- var keyContainer = row.Q("key-container");
- if (keyContainer != null)
- ApplyKeyColumnWidth(keyContainer);
+ if (view != this)
+ view.SyncColumnFractionFromSibling(fraction);
}
}
+ void SyncColumnFractionFromSibling(float fraction)
+ {
+ if (m_ColumnResizer == null)
+ return;
+ m_ColumnResizer.SetKeyColumnFraction(fraction);
+ UpdateColumnWidths();
+ }
+
// Marks that the keys may have changed and queues a single deferred pass.
// Calling this again before the pass runs has no extra effect. The actual
// decision (hash check + interaction gate) lives in SortIfNeeded, which
@@ -755,7 +1120,7 @@ void ScheduleSort(long delayMs)
// If the keys did not actually change we don't want to pay the
// cost of sorting as this can be expensive for a large dictionary
// If the order matches what we already have we skip the (expensive) ListView rebuild and only refresh the
- // duplicate markers, which is much cheaper for large dictionaries.
+ // key warning markers, which is much cheaper for large dictionaries.
void SortIfNeeded()
{
// If the user is interacting with the Editor we wait sorting to prevent disrupting the workflow
@@ -776,10 +1141,10 @@ void SortIfNeeded()
if (m_SortedIndexMap.DisplayOrderEquals(updatedSortedIndexMap))
{
- if (UpdateDuplicateIndicesOnly())
+ if (UpdateMarkerIndicesOnly())
{
UpdateHeaderInfo();
- UpdateDuplicateKeyIconsOnVisibleItems();
+ UpdateKeyWarningIconsOnVisibleItems();
}
return;
}
@@ -791,44 +1156,197 @@ void SortIfNeeded()
FocusContentContainer();
}
+ // Single entry point for every layout change from the context menu. Persists the
+ // user's choice (layoutSetByUser) so it wins over the attribute default, then runs
+ // ApplyLayoutMode, which rebuilds the row pool with the new layout's template. The
+ // rebuild only ever recreates the handful of virtualized visible rows, so it scales
+ // to large dictionaries.
+ void AppendLayoutAction(DropdownMenu menu, string label, DictionaryLayout layout)
+ {
+ menu.AppendAction(label,
+ _ => SetLayout(layout),
+ _ => m_Layout == layout ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
+ }
+
+ void SetLayout(DictionaryLayout layout)
+ {
+ if (m_Layout == layout)
+ return;
+ ApplyLayout(layout);
+ DictionaryDrawer.UpdateCachedState(m_StateCacheKey, state =>
+ {
+ state.layout = layout;
+ state.layoutSetByUser = true;
+ });
+ PerformActionOnLinkedViews(v => v.ApplyLayout(layout));
+ }
+
+ void ApplyLayout(DictionaryLayout layout)
+ {
+ if (!m_IsBound || m_Layout == layout)
+ return;
+ m_Layout = layout;
+ ApplyLayoutMode();
+ }
+
+ // Installs the row template (makeItem) for the current layout without rebuilding.
+ // Called before the initial row build in RebuildFromProperty; ApplyLayoutMode owns the
+ // rebuild when the layout (and thus the template) changes at runtime.
+ void SyncMakeItemToLayout()
+ {
+ m_MakeItemLayout = m_Layout;
+ makeItem = m_Layout switch
+ {
+ DictionaryLayout.OneColumnWithValueFoldout => MakeOneColumnFoldoutRow,
+ DictionaryLayout.OneColumnWithValueVisible => MakeOneColumnVisibleRow,
+ _ => MakeTwoColumnRow,
+ };
+ }
+
+ // Applies the layout mode. Each layout uses a distinct row template (two-column, one-column
+ // foldout, one-column static value), so any layout change swaps makeItem and rebuilds the
+ // pool — heavier than an in-place rebind, but a rare user-initiated switch, and virtualization
+ // means the pool only ever holds the handful of visible rows. The sorted-index map, property
+ // tracking, and ignored-key state are layout-independent.
+ // Pass refresh: false when a list reload already runs alongside this call, so the visible rows
+ // aren't rebuilt/rebound twice in a single layout change. Returns whether the template changed,
+ // so a caller that deferred the refresh (refresh: false) knows it must Rebuild() rather than
+ // RefreshItems() — the stale row pool is the wrong template.
+ bool ApplyLayoutMode(bool refresh = true)
+ {
+ EnableInClassList(k_OneColumnModeClass, m_OneColumnMode);
+ ApplyHeaderLayoutMode();
+
+ bool templateChanged = m_MakeItemLayout != m_Layout;
+ if (templateChanged)
+ {
+ SyncMakeItemToLayout();
+ if (refresh)
+ Rebuild();
+ }
+ else if (refresh)
+ {
+ RefreshItems();
+ }
+ return templateChanged;
+ }
+
+ // The slim one-column header (value label + resizer hidden, key header spanning
+ // full width) is driven by USS on the list header's --one-column modifier. C# only has to
+ // clear the resizer-driven inline widths that ApplyHeaderColumnLayout stamps on
+ // the key/value header in two-column mode — otherwise those inline widths would
+ // outrank the USS flex-grow and pin the slim header to a stale column width.
+ // Returning to two-column recomputes them.
+ void ApplyHeaderLayoutMode()
+ {
+ if (m_ListHeader == null)
+ return;
+
+ // Per-view marker that scopes the slim-header USS to this header only (see
+ // k_ListHeaderOneColumnClass) instead of the leaky view-root --one-column class.
+ m_ListHeader.EnableInClassList(k_ListHeaderOneColumnClass, m_OneColumnMode);
+
+ // The single header column spans both key and value in one-column mode, so it
+ // reads "Key & Value" (honoring any DictionaryDisplayAttribute label overrides).
+ if (m_KeyHeaderLabel != null)
+ m_KeyHeaderLabel.text = m_OneColumnMode
+ ? DictionaryDrawer.Texts.GetOneColumnHeaderLabel(m_KeyLabelText, m_ValueLabelText)
+ : m_KeyLabelText;
+
+ if (m_OneColumnMode)
+ {
+ if (m_KeyHeader != null)
+ {
+ m_KeyHeader.style.width = StyleKeyword.Null;
+ m_KeyHeader.style.flexBasis = StyleKeyword.Null;
+ }
+ if (m_ValueLabel != null)
+ {
+ m_ValueLabel.style.width = StyleKeyword.Null;
+ m_ValueLabel.style.flexBasis = StyleKeyword.Null;
+ }
+ }
+ else
+ {
+ ApplyHeaderColumnLayout();
+ }
+ }
+
void OnKeyHeaderClicked(ClickEvent evt)
{
- m_SortAscending = !m_SortAscending;
+ if (DictionaryDrawer.ShowSerializedOrder)
+ return;
+
+ bool ascending = !m_SortAscending;
+ ApplySortAscending(ascending);
+ DictionaryDrawer.UpdateCachedState(m_StateCacheKey, state => state.sortAscending = ascending);
+ PerformActionOnLinkedViews(v => v.ApplySortAscending(ascending));
+ }
+
+ void ApplySortAscending(bool ascending)
+ {
+ if (!m_IsBound || m_SortAscending == ascending)
+ return;
+ m_SortAscending = ascending;
UpdateSortIndicatorClass();
- DictionaryDrawer.UpdateCachedState(m_StateCacheKey, state => state.sortAscending = m_SortAscending);
RebuildSortedIndicesAndRefresh();
}
void ResetToDefaults()
{
DictionaryDrawer.ClearCachedState(m_StateCacheKey);
+ ApplyResetToDefaults();
+ PerformActionOnLinkedViews(v => v.ApplyResetToDefaults());
+ }
+
+ void ApplyResetToDefaults()
+ {
+ if (!m_IsBound)
+ return;
m_SortAscending = true;
UpdateSortIndicatorClass();
+ // Cache was just cleared, so the active layout reverts to the attribute default.
+ m_Layout = m_AttributeLayout;
+ // RebuildSortedIndicesAndRefresh() below performs the single binding pass. When the
+ // reset changes the template, that pass must Rebuild() the row pool: a plain
+ // RefreshItems() would rebind the recycled rows of the previous template (e.g. stacked
+ // one-column rows) without recreating them, corrupting the layout.
+ bool templateChanged = ApplyLayoutMode(refresh: false);
+
m_ColumnResizer?.ResetToDefaultFraction();
- RebuildSortedIndicesAndRefresh();
+ UpdateColumnWidths();
+ RebuildSortedIndicesAndRefresh(rebuild: templateChanged);
}
void UpdateSortIndicatorClass()
{
+ if (DictionaryDrawer.ShowSerializedOrder)
+ {
+ m_KeyHeader.EnableInClassList(MultiColumnHeaderColumn.sortedAscendingUssClassName, false);
+ m_KeyHeader.EnableInClassList(MultiColumnHeaderColumn.sortedDescendingUssClassName, false);
+ return;
+ }
+
m_KeyHeader.EnableInClassList(MultiColumnHeaderColumn.sortedAscendingUssClassName, m_SortAscending);
m_KeyHeader.EnableInClassList(MultiColumnHeaderColumn.sortedDescendingUssClassName, !m_SortAscending);
}
- void UpdateDuplicateKeyIconVisibility(VisualElement rowElement, int arrayIndex)
+ void UpdateKeyWarningIconVisibility(DictionaryRow row, int arrayIndex)
{
- var icon = rowElement?.Q(className: k_DuplicateKeyIconClass);
+ var icon = row?.keyWarningIcon;
if (icon == null)
return;
- if (DictionaryKeyUtility.GetMarkerKind(arrayIndex, m_DuplicateEntryIndices) != DictionaryKeyUtility.KeyMarkerKind.None)
+ var markerKind = DictionaryKeyUtility.GetMarkerKind(arrayIndex, m_DuplicateEntryIndices, m_NullKeyEntryIndices);
+ if (markerKind != DictionaryKeyUtility.KeyMarkerKind.None)
{
// Icon, size, and top-offset all live in USS on
// .unity-dictionary-view__duplicate-key-icon; we only flip display + tooltip
- // from C# based on duplicate-state.
+ // from C# based on the marker kind.
icon.style.display = DisplayStyle.Flex;
- icon.tooltip = DictionaryDrawer.Texts.DuplicateMarkerTooltip;
+ icon.tooltip = DictionaryKeyUtility.GetMarkerTooltip(markerKind);
}
else
{
@@ -839,17 +1357,17 @@ void UpdateDuplicateKeyIconVisibility(VisualElement rowElement, int arrayIndex)
// Thin wrapper around the shared in-place refresh so call sites stay
// self-documenting at the UITK layer.
- bool UpdateDuplicateIndicesOnly()
- => DictionaryDrawer.TryRefreshDuplicateIndicesInto(m_DictionaryFieldProperty, m_DuplicateEntryIndices);
+ bool UpdateMarkerIndicesOnly()
+ => DictionaryDrawer.TryRefreshDuplicateAndNullKeyIndicesInto(
+ m_DictionaryFieldProperty, m_DuplicateEntryIndices, m_NullKeyEntryIndices);
- void UpdateDuplicateKeyIconsOnVisibleItems()
+ void UpdateKeyWarningIconsOnVisibleItems()
{
var content = scrollView.contentContainer;
foreach (var wrapper in content.Children())
{
- var dictElement = wrapper.Q("dict-element");
- if (dictElement != null && dictElement.userData is int displayIndex && displayIndex < displayedItemCount)
- UpdateDuplicateKeyIconVisibility(dictElement, DisplayToArrayIndex(displayIndex));
+ if (wrapper.Q() is { } row && row.displayIndex >= 0 && row.displayIndex < displayedItemCount)
+ UpdateKeyWarningIconVisibility(row, DisplayToArrayIndex(row.displayIndex));
}
}
@@ -898,7 +1416,6 @@ void OnAddClicked()
schedule.Execute(() => ScrollToItem(newDisplayIndex));
UpdateHeaderInfo();
- UpdateRemoveButtonState();
}
void RebuildSortedIndices()
@@ -907,11 +1424,13 @@ void RebuildSortedIndices()
m_HashOfKeys = DictionaryDrawer.GetKeysContentHash(m_ArrayProperty);
}
- void RebuildSortedIndicesAndRefresh()
+ // Pass rebuild: true when the row template changed (one-column <-> two-column axis flip),
+ // so the binding pass recreates the row pool instead of rebinding stale rows in place.
+ void RebuildSortedIndicesAndRefresh(bool rebuild = false)
{
var selectedArrayIndices = GetSelectedArrayIndices();
RebuildSortedIndices();
- RefreshListView();
+ RefreshListView(rebuild);
RestoreSelectionByArrayIndices(selectedArrayIndices);
FocusContentContainer();
}
@@ -951,13 +1470,18 @@ void RestoreSelectionByArrayIndices(List arrayIndices)
}
}
- void RefreshListView()
+ void RefreshListView(bool rebuild = false)
{
- UpdateDuplicateIndicesOnly();
+ UpdateMarkerIndicesOnly();
UpdateHeaderInfo();
UpdateListViewItemsSource(displayedItemCount);
- RefreshItems();
+ // Rebuild() recreates the row pool from the current makeItem; RefreshItems() only
+ // rebinds the existing rows. A template-axis flip needs the former (see ApplyLayoutMode).
+ if (rebuild)
+ Rebuild();
+ else
+ RefreshItems();
}
// Keeps m_ItemsSource sized to arraySize and wired up as the
@@ -982,25 +1506,35 @@ void UpdateHeaderInfo()
{
int itemCount = m_ArrayProperty.arraySize;
int duplicateCount = m_DuplicateEntryIndices.Count;
- bool hasDuplicates = duplicateCount > 0;
+ int nullKeyCount = m_NullKeyEntryIndices.Count;
+ int ignoredCount = duplicateCount + nullKeyCount;
+ bool hasIgnored = ignoredCount > 0;
if (m_HeaderInfoLabel != null)
{
- string text = DictionaryDrawer.Texts.GetItemCountText(itemCount);
- if (hasDuplicates)
- text += DictionaryDrawer.Texts.GetDuplicateCountText(duplicateCount);
+ string text;
+ if (DictionaryDrawer.ShowSerializedOrder)
+ {
+ text = DictionaryDrawer.Texts.ShowingSerializedOrderInfoLabel;
+ }
+ else
+ {
+ text = DictionaryDrawer.Texts.GetItemCountText(itemCount);
+ if (hasIgnored)
+ text += DictionaryDrawer.Texts.GetIgnoredCountText(ignoredCount);
+ }
m_HeaderInfoLabel.text = text;
}
- if (m_DuplicatesHelpBox != null)
+ if (m_IgnoredHelpBox != null)
{
- if (hasDuplicates)
+ if (hasIgnored)
{
- m_DuplicatesHelpBox.text = DictionaryDrawer.Texts.GetDuplicatesHelpBoxText(duplicateCount);
- m_DuplicatesHelpBox.style.display = DisplayStyle.Flex;
+ m_IgnoredHelpBox.text = DictionaryDrawer.Texts.GetIgnoredHelpBoxText(duplicateCount, nullKeyCount);
+ m_IgnoredHelpBox.style.display = DisplayStyle.Flex;
}
else
{
- m_DuplicatesHelpBox.style.display = DisplayStyle.None;
+ m_IgnoredHelpBox.style.display = DisplayStyle.None;
}
}
}
@@ -1010,8 +1544,9 @@ void OnRemoveClicked()
var selected = SelectedDisplayIndices;
int newSelectedDisplayIndex = selected.Count == 1 ? selected[0] : -1;
- bool removed = DictionaryDrawer.RemoveEntriesAtDisplayIndices(
- m_ArrayProperty, selected, m_SortedIndexMap);
+ var removed = selected.Count > 0 ?
+ DictionaryDrawer.RemoveEntriesAtDisplayIndices(m_ArrayProperty, selected, m_SortedIndexMap) :
+ DictionaryDrawer.RemoveEntryAtDisplayIndex(m_ArrayProperty, m_ArrayProperty.arraySize - 1, m_SortedIndexMap);
if (!removed)
return;
@@ -1026,15 +1561,6 @@ void OnRemoveClicked()
SetSelection(Mathf.Min(newSelectedDisplayIndex, newSize - 1));
FocusContentContainer();
- UpdateRemoveButtonState();
- }
-
- void UpdateRemoveButtonState()
- {
- // BaseListView.allowRemove drives the built-in remove button's enabled
- // state through UpdateRemoveButton(), which honors both this flag and
- // the item count — same UX as the previous hand-built footer.
- allowRemove = SelectedDisplayIndices.Count > 0;
}
///
@@ -1051,16 +1577,9 @@ sealed class ColumnResizer
// here, for the drag-start width calculation.
const float k_ResizerLineWidth = 1f;
- // In-memory, editor-process lifetime. Key: same Hash128 as s_StateCache (normalized path).
- // Holds the set of ColumnResizer instances across which fraction changes propagate.
- // Eviction: per-resizer in OnDetachFromPanel; entry removed when its list empties.
- // Registration is gated on m_IsPartOfList, so top-level dictionaries never enter this table.
- static readonly Dictionary> s_LinkedResizers = new();
-
readonly VisualElement m_Header;
readonly Hash128 m_StateCacheKey;
- readonly Action m_OnFractionChanged;
- readonly bool m_IsPartOfList;
+ readonly Action m_OnDragged;
readonly float m_AttributeFraction;
float m_KeyColumnFraction;
@@ -1071,11 +1590,10 @@ sealed class ColumnResizer
public float KeyColumnFraction => m_KeyColumnFraction;
- public ColumnResizer(VisualElement header, string propertyPath, float attributeFraction, Action onFractionChanged)
+ public ColumnResizer(VisualElement header, string propertyPath, float attributeFraction, Action onDragged)
{
m_Header = header;
- m_OnFractionChanged = onFractionChanged;
- m_IsPartOfList = Regex.IsMatch(propertyPath, @"\[\d+\]");
+ m_OnDragged = onDragged;
m_AttributeFraction = attributeFraction;
m_StateCacheKey = DictionaryDrawer.ComputeStateCacheKey(propertyPath);
m_KeyColumnFraction = DictionaryDrawer.GetActiveKeyColumnFraction(m_StateCacheKey, attributeFraction);
@@ -1099,55 +1617,17 @@ public VisualElement BuildElement()
interaction.RegisterCallback(OnPointerUp);
interaction.RegisterCallback(OnPointerCaptureOut);
- if (m_IsPartOfList)
- {
- line.RegisterCallback(OnAttachToPanel);
- line.RegisterCallback(OnDetachFromPanel);
- }
-
return line;
}
- public void ResetToDefaultFraction()
+ public void SetKeyColumnFraction(float fraction)
{
- m_KeyColumnFraction = m_AttributeFraction;
- m_OnFractionChanged?.Invoke();
- if (m_IsPartOfList)
- NotifyLinkedResizers();
+ m_KeyColumnFraction = fraction;
}
- void OnAttachToPanel(AttachToPanelEvent evt)
- {
- if (!s_LinkedResizers.TryGetValue(m_StateCacheKey, out var list))
- {
- list = new List();
- s_LinkedResizers[m_StateCacheKey] = list;
- }
- list.Add(this);
- }
-
- void OnDetachFromPanel(DetachFromPanelEvent evt)
- {
- if (s_LinkedResizers.TryGetValue(m_StateCacheKey, out var list))
- {
- list.Remove(this);
- if (list.Count == 0)
- s_LinkedResizers.Remove(m_StateCacheKey);
- }
- }
-
- void NotifyLinkedResizers()
+ public void ResetToDefaultFraction()
{
- if (!s_LinkedResizers.TryGetValue(m_StateCacheKey, out var list) || list.Count <= 1)
- return;
-
- foreach (var resizer in list)
- {
- if (resizer == this)
- continue;
- resizer.m_KeyColumnFraction = m_KeyColumnFraction;
- resizer.m_OnFractionChanged?.Invoke();
- }
+ m_KeyColumnFraction = m_AttributeFraction;
}
void OnPointerDown(PointerDownEvent evt)
@@ -1172,9 +1652,7 @@ void OnPointerMove(PointerMoveEvent evt)
float deltaX = evt.position.x - m_DragStartX;
float newFraction = m_DragStartFraction + deltaX / m_DragStartHeaderContentWidth;
m_KeyColumnFraction = DictionaryDrawer.ClampDraggedKeyColumnFraction(newFraction, m_DragStartHeaderContentWidth);
- m_OnFractionChanged?.Invoke();
- if (m_IsPartOfList)
- NotifyLinkedResizers();
+ m_OnDragged?.Invoke();
evt.StopPropagation();
}
diff --git a/Editor/Mono/GUI/TreeView/TreeViewControl/TreeViewOld.cs b/Editor/Mono/GUI/TreeView/TreeViewControl/TreeViewOld.cs
index 3a29e3bc0a..1002195d6c 100644
--- a/Editor/Mono/GUI/TreeView/TreeViewControl/TreeViewOld.cs
+++ b/Editor/Mono/GUI/TreeView/TreeViewControl/TreeViewOld.cs
@@ -9,7 +9,7 @@
namespace UnityEditor.IMGUI.Controls;
-[Obsolete("TreeView is now deprecated. You can likely now use TreeView instead and not think more about it. But if you were using that identifier to store InstanceID data, you should instead opt to upgrade your TreeViews to use TreeView to get the proper typing.", true)]
+[Obsolete($"TreeView is now deprecated. You can likely now use TreeView instead and not think more about it. But if you were using that identifier to store {nameof(EntityId)} data, you should instead opt to upgrade your TreeViews to use TreeView<{nameof(EntityId)}> to get the proper typing.", true)]
public abstract partial class TreeView : TreeViewInternal
{
protected TreeView(TreeViewState state)
@@ -176,7 +176,7 @@ public static implicit operator CanStartDragArgs(TreeView.CanStartDragArgs
protected static bool IsChildListForACollapsedParent(IList childList) => TreeView.IsChildListForACollapsedParent(childList.ToGenericIList());
}
-[Obsolete("TreeView is now deprecated. You can likely now use TreeView instead and not think more about it. But if you were using that identifier to store InstanceID data, you should instead opt to upgrade your TreeViews to use TreeView to get the proper typing.", true)]
+[Obsolete($"TreeView is now deprecated. You can likely now use TreeView instead and not think more about it. But if you were using that identifier to store {nameof(EntityId)} data, you should instead opt to upgrade your TreeViews to use TreeView<{nameof(EntityId)}> to get the proper typing.", true)]
public abstract class TreeViewInternal : TreeView
{
protected TreeViewInternal(TreeViewState state)
@@ -231,7 +231,7 @@ public GenericTreeViewItemToNonGenericTreeViewItemIList(IList>
}
}
-[Obsolete("TreeViewItem is now deprecated. You can likely now use TreeViewItem instead and not think more about it. But if you were using that identifier to store InstanceID data, you should instead opt to upgrade your TreeViews to use TreeViewItem to get the proper typing.", true)]
+[Obsolete($"TreeViewItem is now deprecated. You can likely now use TreeViewItem instead and not think more about it. But if you were using that identifier to store {nameof(EntityId)} data, you should instead opt to upgrade your TreeViews to use TreeViewItem<{nameof(EntityId)}> to get the proper typing.", true)]
public class TreeViewItem : TreeViewItem
{
public new virtual List children
@@ -269,7 +269,7 @@ public TreeViewItem(int id, int depth, string displayName) : base(id, depth, dis
internal TreeViewItem(int id, int depth, TreeViewItem parent, string displayName) : base(id, depth, parent, displayName) { }
}
-[Obsolete("TreeViewState is now deprecated. You can likely now use TreeViewState instead and not think more about it. But if you were using that identifier to store InstanceID data, you should instead opt to upgrade your TreeViews to use TreeViewState to get the proper typing.", true)]
+[Obsolete($"TreeViewState is now deprecated. You can likely now use TreeViewState instead and not think more about it. But if you were using that identifier to store {nameof(EntityId)} data, you should instead opt to upgrade your TreeViews to use TreeViewState<{nameof(EntityId)}> to get the proper typing.", true)]
public class TreeViewState : TreeViewState {}
[Obsolete]
diff --git a/Editor/Mono/GUI/WindowLayout.cs b/Editor/Mono/GUI/WindowLayout.cs
index fce60fdacb..79f6d02268 100644
--- a/Editor/Mono/GUI/WindowLayout.cs
+++ b/Editor/Mono/GUI/WindowLayout.cs
@@ -1765,10 +1765,10 @@ static void ShowInspector()
[MenuItem("Window/General/Hierarchy %4", false, 4)]
static void ShowNewHierarchy()
{
- if (HierarchyPreferences.UseNewHierarchy)
- EditorWindow.GetWindow(HierarchyPreferences.HierarchyV2WindowType);
- else
+ if (EditorSettings.useLegacyHierarchy)
EditorWindow.GetWindow();
+ else
+ EditorWindow.GetWindow(HierarchyPreferences.HierarchyV2WindowType);
}
[MenuItem("Window/General/Project %5", false, 5)]
diff --git a/Editor/Mono/GUIView.bindings.cs b/Editor/Mono/GUIView.bindings.cs
index e4d752579d..03cb28b263 100644
--- a/Editor/Mono/GUIView.bindings.cs
+++ b/Editor/Mono/GUIView.bindings.cs
@@ -153,6 +153,16 @@ internal extern bool hdrActive
[UnityMarshalThisAs(NativeType.Custom, CustomMarshaller = typeof(NativeHandleMarshaller))]
internal extern void GrabPixels(RenderTexture rd, Rect rect);
+ // Null until the window has rendered once.
+ [NativeMethod("MonoGUIView::GetAuxBackBufferTexture", HasExplicitThis = true)]
+ [UnityMarshalThisAs(NativeType.Custom, CustomMarshaller = typeof(NativeHandleMarshaller))]
+ internal extern RenderTexture GetAuxBackBufferTexture();
+
+ // True when the aux back buffer's texel row 0 is the top of the window.
+ [NativeMethod("MonoGUIView::GetAuxBackBufferTextureIsTopOrigin", HasExplicitThis = true)]
+ [UnityMarshalThisAs(NativeType.Custom, CustomMarshaller = typeof(NativeHandleMarshaller))]
+ internal extern bool GetAuxBackBufferTextureIsTopOrigin();
+
[NativeMethod("MonoGUIView::GetBackingScaleFactor", HasExplicitThis = true)]
[UnityMarshalThisAs(NativeType.Custom, CustomMarshaller = typeof(NativeHandleMarshaller))]
internal extern float GetBackingScaleFactor();
diff --git a/Editor/Mono/GameView/StatsField.cs b/Editor/Mono/GameView/StatsField.cs
index e62f93d41a..6fa7bd43b7 100644
--- a/Editor/Mono/GameView/StatsField.cs
+++ b/Editor/Mono/GameView/StatsField.cs
@@ -109,8 +109,8 @@ public StatsData()
[CreateProperty] public string hybridBatcherDrawInfo => $"{UnityStats.hybridBatcherDrawCalls} draw calls ({UnityStats.hybridBatcherInstances} instances)";
[CreateProperty] public string standardDrawInfo => $"{UnityStats.standardDrawCalls} draw calls ({UnityStats.standardInstances} instances)";
[CreateProperty] public string standardInstancedDrawInfo => $"{UnityStats.standardInstancedDrawCalls} draw calls ({UnityStats.standardInstancedInstances} instances)";
- [CreateProperty] public string triangles => FormatCounts(UnityStats.triangles);
- [CreateProperty] public string vertices => FormatCounts(UnityStats.vertices);
+ [CreateProperty] public string triangles => FormatCounts(UnityStats.trianglesLong);
+ [CreateProperty] public string vertices => FormatCounts(UnityStats.verticesLong);
[CreateProperty] public string desiredTextureMemory => $"{Texture.desiredTextureMemory * k_BytesToMegabytes:F1} MB";
// UnityStats
@@ -144,11 +144,15 @@ public StatsData()
[CreateProperty] public int animatorComponentsPlaying => UnityStats.animatorComponentsPlaying;
- private string FormatCounts(int value)
+ private string FormatCounts(long value)
{
- if (value >= 1000)
+ if (value >= 1_000_000)
{
- return $"{value / 1000.0f:F1}k";
+ return $"{value / 1_000_000.0f:F1}M";
+ }
+ else if (value >= 1_000)
+ {
+ return $"{value / 1_000.0f:F1}k";
}
return value.ToString();
}
diff --git a/Editor/Mono/GenerateIconsWithMipLevels.cs b/Editor/Mono/GenerateIconsWithMipLevels.cs
index 08db889e4f..15e62a8931 100644
--- a/Editor/Mono/GenerateIconsWithMipLevels.cs
+++ b/Editor/Mono/GenerateIconsWithMipLevels.cs
@@ -309,7 +309,15 @@ private static bool BlitMip(Texture2D iconWithMips, List sortedTextur
Texture2D tex = sortedTextures[mipLevel];
if (tex)
{
- Blit(tex, iconWithMips, mipLevel);
+ try
+ {
+ Blit(tex, iconWithMips, mipLevel);
+ }
+ catch(Exception ex)
+ {
+ Debug.LogException(new Exception("Failed to blit mip level: " + mipLevel + " for texture: " + tex.name, ex));
+ return false;
+ }
return true;
}
else
diff --git a/Editor/Mono/Graphics/Analytics/GraphicsToolLifetimeAnalytic.cs b/Editor/Mono/Graphics/Analytics/GraphicsToolLifetimeAnalytic.cs
index b591788df7..8a3b0338c3 100644
--- a/Editor/Mono/Graphics/Analytics/GraphicsToolLifetimeAnalytic.cs
+++ b/Editor/Mono/Graphics/Analytics/GraphicsToolLifetimeAnalytic.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.Analytics;
@@ -12,7 +13,7 @@ namespace UnityEditor.Rendering.Analytics
{
// schema = com.unity3d.data.schemas.editor.analytics.uGraphicsToolLifetimeAnalytic_v2
// taxonomy = editor.analytics.uGraphicsToolLifetimeAnalytic.v2
- public class GraphicsToolLifetimeAnalytic
+ public partial class GraphicsToolLifetimeAnalytic
{
static bool IsInternalAssembly(Type type)
{
@@ -91,6 +92,7 @@ internal struct WindowOpenedMetadata
public DateTime openedTime;
}
+ [AutoStaticsCleanupOnCodeReload(CleanupStrategy = CleanupStrategy.Clear)]
static Dictionary s_WindowOpenedMetadata = new Dictionary();
public static void WindowOpened()
diff --git a/Editor/Mono/Graphics/D3D12DeviceFilterListsEditor.cs b/Editor/Mono/Graphics/D3D12DeviceFilterListsEditor.cs
index f065baa831..265a7261a1 100644
--- a/Editor/Mono/Graphics/D3D12DeviceFilterListsEditor.cs
+++ b/Editor/Mono/Graphics/D3D12DeviceFilterListsEditor.cs
@@ -22,6 +22,7 @@ internal static class Styles
public const float kHeightBetweenFields = 3.0f;
public const float kHeightBetweenRows = 10.0f;
public const float kComparatorFieldWidth = 170.0f;
+ public const string kGfxJobsNoticeText = "The order of the Graphics Jobs Filters is important. Filtering will use the first passing filter to determine Graphics Jobs Mode at runtime.";
public static readonly GUIContent preferredGraphicsJobsMode = EditorGUIUtility.TrTextContent("Preferred Graphics Jobs Mode", "Indicates which graphics jobs mode this filter will enforce at runtime.");
@@ -460,47 +461,35 @@ struct ErrorInfo
}
};
- private float DoListInternal(ReorderableFilterList list, float startingHeight)
+ private void DoListInternal(ReorderableFilterList list)
{
- var listRect = GUILayoutUtility.GetRect(startingHeight, list.reorderableList.GetHeight(), GUILayout.ExpandWidth(true));
+ var listRect = GUILayoutUtility.GetRect(0.0f, list.reorderableList.GetHeight(), GUILayout.ExpandWidth(true));
listRect.x += EditorGUI.kIndentPerLevel;
listRect.width -= EditorGUI.kIndentPerLevel;
list.DoList(listRect);
- return list.reorderableList.GetHeight();
}
- private float DoList(ReorderableFilterList list, string name, ref bool showPosition, float startingHeight, Func onBeforeListDraw = null, Func onAfterListDraw = null)
+ private void DoList(ReorderableFilterList list, string name, ref bool showPosition, Action onBeforeListDraw = null, Action onAfterListDraw = null)
{
- var height = startingHeight;
showPosition = EditorGUILayout.BeginFoldoutHeaderGroup(showPosition, name);
if (showPosition)
{
- using (var scopedHeight = new IndentLevelScope())
+ using (new IndentLevelScope())
{
- height += onBeforeListDraw?.Invoke(height) ?? 0.0f;
- height += DoListInternal(list, height);
- height += onAfterListDraw?.Invoke(height) ?? 0.0f;
+ onBeforeListDraw?.Invoke();
+ DoListInternal(list);
+ onAfterListDraw?.Invoke();
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
- return height;
}
- private float DrawGfxJobsExtraNotice(float startingHeight)
+ private void DrawGfxJobsExtraNotice()
{
- var content = EditorGUIUtility.TempContent("The order of the Graphics Jobs Filters is important. Filtering will use the first passing filter to determine Graphics Jobs Mode at runtime.", EditorGUIUtility.GetHelpIcon(MessageType.Info));
+ var content = EditorGUIUtility.TempContent(D3D12DeviceFilterUI.Styles.kGfxJobsNoticeText, EditorGUIUtility.GetHelpIcon(MessageType.Info));
- var rect = GUILayoutUtility.GetRect(0.0f, 0.0f, GUILayout.ExpandWidth(true));
- rect.x += EditorGUI.kIndentPerLevel;
- rect.width -= EditorGUI.kIndentPerLevel;
- var height = EditorStyles.helpBox.CalcHeight(content, rect.width);
-
- rect = GUILayoutUtility.GetRect(0.0f, height, GUILayout.ExpandWidth(true));
- rect.x += EditorGUI.kIndentPerLevel;
- rect.width -= EditorGUI.kIndentPerLevel;
-
- EditorGUI.HelpBox(rect, content);
- return height;
+ // CalcHeight here would measure the Layout-event dummy rect width and clip the box.
+ EditorGUILayout.HelpBox(content);
}
public override void OnInspectorGUI()
@@ -513,11 +502,9 @@ public override void OnInspectorGUI()
using (var changed = new ChangeCheckScope())
{
- var height = 0.0f;
-
- height = DoList(m_AllowReorderableFilterList, "Allow Filters", ref m_ShowAllow, height);
- height = DoList(m_DenyReorderableFilterList, "Deny Filters", ref m_ShowDeny, height);
- height = DoList(m_GfxJobsReorderableFilterList, "Preferred Graphics Jobs Filters", ref m_ShowGfxJobs, height, DrawGfxJobsExtraNotice);
+ DoList(m_AllowReorderableFilterList, "Allow Filters", ref m_ShowAllow);
+ DoList(m_DenyReorderableFilterList, "Deny Filters", ref m_ShowDeny);
+ DoList(m_GfxJobsReorderableFilterList, "Preferred Graphics Jobs Filters", ref m_ShowGfxJobs, DrawGfxJobsExtraNotice);
if (changed.changed)
serializedObject.ApplyModifiedProperties();
diff --git a/Editor/Mono/Graphics/GraphicsStateCollectionImporter.cs b/Editor/Mono/Graphics/GraphicsStateCollectionImporter.cs
index cfa5138080..221f048a9e 100644
--- a/Editor/Mono/Graphics/GraphicsStateCollectionImporter.cs
+++ b/Editor/Mono/Graphics/GraphicsStateCollectionImporter.cs
@@ -155,6 +155,8 @@ public static GUIStyle greyFoldoutStyle
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_RuntimePlatform = serializedObject.FindProperty("runtimePlatform");
m_GraphicsDeviceType = serializedObject.FindProperty("graphicsDeviceType");
diff --git a/Editor/Mono/Handles/HandleUtility.cs b/Editor/Mono/Handles/HandleUtility.cs
index b8620b65f2..c86e353288 100644
--- a/Editor/Mono/Handles/HandleUtility.cs
+++ b/Editor/Mono/Handles/HandleUtility.cs
@@ -170,6 +170,19 @@ internal static float GetParametrization(Vector2 x0, Vector2 x1, Vector2 x2)
return -(Vector2.Dot(x1 - x0, x2 - x1) / (x2 - x1).sqrMagnitude);
}
+ internal static Vector3 WorldPointWithScreenOffset(Camera cam, Vector3 worldPoint, Vector2 screenOffset)
+ {
+ Transform camTransform = cam.transform;
+ Vector2 screenPoint = cam.WorldToScreenPoint(worldPoint);
+ Vector2 rightPixels = (Vector2)cam.WorldToScreenPoint(worldPoint + camTransform.right) - screenPoint;
+ Vector2 upPixels = (Vector2)cam.WorldToScreenPoint(worldPoint + camTransform.up) - screenPoint;
+
+ float alongRight = rightPixels.sqrMagnitude > Mathf.Epsilon ? screenOffset.x / rightPixels.magnitude : 0f;
+ float alongUp = upPixels.sqrMagnitude > Mathf.Epsilon ? screenOffset.y / upPixels.magnitude : 0f;
+
+ return worldPoint + camTransform.right * alongRight + camTransform.up * alongUp;
+ }
+
// This limits the "shoot off into infinity" factor when the cursor ray and constraint are near parallel.
// Increase this value to more conservatively restrict movement, lower to allow more extreme values.
// Ex, with a camera roughly 30 degrees to the handle a value of .1 restricts translation to ~1500m, whereas a
diff --git a/Editor/Mono/Help.cs b/Editor/Mono/Help.cs
index 7ae34d3776..0c9dbbbf69 100644
--- a/Editor/Mono/Help.cs
+++ b/Editor/Mono/Help.cs
@@ -41,6 +41,7 @@ public partial class Help
internal static string k_AlphaReleaseNotesUrlBase = "https://unity3d.com/unity/alpha/";
internal static string k_BetaReleaseNotesUrlBase = "https://unity3d.com/unity/beta/";
internal static string k_ReleaseNotesUrlBase = "https://unity3d.com/unity/whats-new/";
+ internal static string k_ThirdPartyBookMark = "#third-party-notices";
internal enum DocRedirectionServer
{
@@ -168,7 +169,7 @@ internal static string GetNiceHelpNameForObject(Object obj, bool defaultToMonoBe
}
else
{
- if (obj is Component || obj is MonoScript)
+ if (InternalEditorUtility.IsUnityAssembly(obj.GetType()) && (obj is Component || obj is MonoScript))
{
return "MonoBehaviour";
}
@@ -218,17 +219,20 @@ internal static string GetHelpURLForObject(Object obj, bool defaultToMonoBehavio
return FindHelpNamed(url);
}
- var topicForObject = HelpFileNameForObject(obj);
- if (HasNamedHelp(topicForObject))
+ if (InternalEditorUtility.IsUnityAssembly(obj.GetType()))
{
- return FindHelpNamed(topicForObject);
- }
+ var topicForObject = HelpFileNameForObject(obj);
+ if (HasNamedHelp(topicForObject))
+ {
+ return FindHelpNamed(topicForObject);
+ }
- if (defaultToMonoBehaviour)
- {
- if (obj is Component || obj is MonoScript)
+ if (defaultToMonoBehaviour)
{
- return FindHelpNamed(k_MonoScriptReference);
+ if (obj is Component || obj is MonoScript)
+ {
+ return FindHelpNamed(k_MonoScriptReference);
+ }
}
}
@@ -384,6 +388,13 @@ internal static void OpenReleaseNotes()
Application.OpenURL(releaseNotesUrl);
}
+ [UnityEngine.Scripting.RequiredByNativeCode]
+ internal static void OpenThirdPartyNotices()
+ {
+ var thirdPartyNoticesUrl = GetReleaseNotesUrl(InternalEditorUtility.GetUnityVersionDigits(), InternalEditorUtility.GetUnityDisplayVersion()) + k_ThirdPartyBookMark;
+ Application.OpenURL(thirdPartyNoticesUrl);
+ }
+
internal static string GetReleaseNotesUrl(string digitsOnlyVersion, string displayVersion)
{
var url = "http://unity3d.com/whatsnew.html";
diff --git a/Editor/Mono/HierarchyPreferences.cs b/Editor/Mono/HierarchyPreferences.cs
index d04b56b0cc..280d2e9848 100644
--- a/Editor/Mono/HierarchyPreferences.cs
+++ b/Editor/Mono/HierarchyPreferences.cs
@@ -10,7 +10,7 @@
namespace UnityEditor
{
- [VisibleToOtherModules("HierarchyModule")]
+ [VisibleToOtherModules("HierarchyModule", "UnityEditor.UIToolkitAuthoringModule", "MultiplayerEditorModule")]
internal static partial class HierarchyPreferences
{
public enum IconMode
@@ -20,8 +20,6 @@ public enum IconMode
GameObjectOnly
}
- const bool kUseNewHierarchy = false;
-
public static PrefabStage.Mode DefaultPrefabModeFromHierarchy
{
get
@@ -34,37 +32,41 @@ public static PrefabStage.Mode DefaultPrefabModeFromHierarchy
}
}
- public static IconMode GameObjectIconMode
- {
- get => (IconMode)s_GameObjectIconMode.value;
- set
- {
- if (s_GameObjectIconMode.value != (int)value)
- {
- s_GameObjectIconMode.value = (int)value;
- GameObjectIconModeChanged?.Invoke();
- }
- }
- }
-
- public static event Action GameObjectIconModeChanged;
public static readonly SavedBool RenameNewObjects = new SavedBool("SceneHierarchyWindow.RenameNewObjects", true);
public static readonly SavedBool UseQueryBuilder = new SavedBool("HierarchyWindow.UseQueryBuilder", true);
public static readonly SavedBool AlternatingRowBackground = new SavedBool("HierarchyWindow.AlternatingRowBackground", true);
- public static readonly SavedBool UseNewHierarchy = new SavedBool("HierarchyWindow.UseNewHierarchy", kUseNewHierarchy);
+ public static readonly SavedBool AllowAlphaNumericHierarchy = new SavedBool("AllowAlphaNumericHierarchy", false);
+ public static readonly SavedInt GameObjectIconMode = new SavedInt("HierarchyWindow.GameObjectIconMode", 0);
- static readonly SavedInt s_GameObjectIconMode = new SavedInt("HierarchyWindow.GameObjectIconMode", 0);
+ ///
+ /// Fired whenever any tracked hierarchy preference value changes.
+ /// Subscribers receive no argument; query the specific preference for the new value.
+ ///
+ [VisibleToOtherModules("HierarchyModule", "MultiplayerEditorModule")]
+ [AutoStaticsCleanupOnCodeReload]
+ internal static event Action AnyPreferenceChanged;
+
+ static void FireAnyPreferenceChanged() => AnyPreferenceChanged?.Invoke();
+
+ static HierarchyPreferences()
+ {
+ RenameNewObjects.valueChanged += FireAnyPreferenceChanged;
+ UseQueryBuilder.valueChanged += FireAnyPreferenceChanged;
+ AlternatingRowBackground.valueChanged += FireAnyPreferenceChanged;
+ EditorSettings.useLegacyHierarchyChanged += FireAnyPreferenceChanged;
+ GameObjectIconMode.valueChanged += FireAnyPreferenceChanged;
+ }
public static void EnsureCorrectHierarchyIsInUse(EditorWindow window)
{
var windowIsLegacy = window is SceneHierarchyWindow;
- if (UseNewHierarchy != windowIsLegacy)
+ if (EditorSettings.useLegacyHierarchy == windowIsLegacy)
return;
- var wndType = UseNewHierarchy ? HierarchyV2WindowType : typeof(SceneHierarchyWindow);
+ var wndType = EditorSettings.useLegacyHierarchy ? typeof(SceneHierarchyWindow) : HierarchyV2WindowType;
var replacementWindow = (EditorWindow)ScriptableObject.CreateInstance(wndType);
- if (window.docked && window.m_Parent is DockArea dockParent)
+ if (window.m_Parent is DockArea dockParent)
dockParent.AddTab(dockParent.m_Panes.IndexOf(window), replacementWindow);
else
{
@@ -74,6 +76,23 @@ public static void EnsureCorrectHierarchyIsInUse(EditorWindow window)
window.Close();
}
+ ///
+ /// Re-reads all hierarchy preferences from the EditorPrefs store and fires
+ /// for any value that has changed since
+ /// the last read. Intended for use by virtual-player clones after
+ /// to pick up changes made in the main editor
+ /// without bypassing the cached layer.
+ ///
+ [VisibleToOtherModules("HierarchyModule", "MultiplayerEditorModule")]
+ internal static void RefreshPreferences()
+ {
+ RenameNewObjects.Refresh();
+ UseQueryBuilder.Refresh();
+ AlternatingRowBackground.Refresh();
+ GameObjectIconMode.Refresh();
+ }
+
+ [VisibleToOtherModules("HierarchyModule", "MultiplayerEditorModule")]
[AutoStaticsCleanupOnCodeReload]
internal static Type HierarchyV2WindowType;
}
diff --git a/Editor/Mono/HostView.cs b/Editor/Mono/HostView.cs
index 044990da7f..49745932fc 100644
--- a/Editor/Mono/HostView.cs
+++ b/Editor/Mono/HostView.cs
@@ -387,10 +387,10 @@ private static IEnumerable GetBuiltInPaneTypes()
yield return typeof(SceneView);
yield return typeof(GameView);
yield return typeof(InspectorWindow);
- if (HierarchyPreferences.UseNewHierarchy)
- yield return HierarchyPreferences.HierarchyV2WindowType;
- else
+ if (EditorSettings.useLegacyHierarchy)
yield return typeof(SceneHierarchyWindow);
+ else
+ yield return HierarchyPreferences.HierarchyV2WindowType;
yield return typeof(ProjectBrowser);
yield return typeof(ProfilerWindow);
if (AnimationWindowCallbacks.AnimationWindowType != null)
diff --git a/Editor/Mono/IHierarchyWindow.cs b/Editor/Mono/IHierarchyWindow.cs
index 2c4feca423..b5e791871f 100644
--- a/Editor/Mono/IHierarchyWindow.cs
+++ b/Editor/Mono/IHierarchyWindow.cs
@@ -23,7 +23,7 @@ internal interface IHierarchyWindow
static IHierarchyWindow GetLastInteractedHierarchyWindow()
{
- if (HierarchyPreferences.UseNewHierarchy)
+ if (!EditorSettings.useLegacyHierarchy)
{
var windows = Resources.FindObjectsOfTypeAll(HierarchyPreferences.HierarchyV2WindowType);
if (windows == null || windows.Length == 0 || windows[0] is not IHierarchyWindow wnd)
@@ -38,7 +38,7 @@ static void GetAllHierarchyWindows(List windows)
{
windows.Clear();
- if (HierarchyPreferences.UseNewHierarchy)
+ if (!EditorSettings.useLegacyHierarchy)
{
var objs = Resources.FindObjectsOfTypeAll(HierarchyPreferences.HierarchyV2WindowType);
if (objs == null || objs.Length == 0)
diff --git a/Editor/Mono/ImportSettings/IHVImageFormatImporterInspector.cs b/Editor/Mono/ImportSettings/IHVImageFormatImporterInspector.cs
index 3a9165ebf9..1f1d2fb8ed 100644
--- a/Editor/Mono/ImportSettings/IHVImageFormatImporterInspector.cs
+++ b/Editor/Mono/ImportSettings/IHVImageFormatImporterInspector.cs
@@ -44,6 +44,8 @@ internal class Styles
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_IsReadable = serializedObject.FindProperty("m_IsReadable");
m_sRGBTexture = serializedObject.FindProperty("m_sRGBTexture");
diff --git a/Editor/Mono/ImportSettings/SpeedTreeImporterInspector.cs b/Editor/Mono/ImportSettings/SpeedTreeImporterInspector.cs
index e5b7f6c617..2d757af28e 100644
--- a/Editor/Mono/ImportSettings/SpeedTreeImporterInspector.cs
+++ b/Editor/Mono/ImportSettings/SpeedTreeImporterInspector.cs
@@ -29,6 +29,12 @@ private static class Styles
public override void OnEnable()
{
+ if (!AreImporterTargetsValid())
+ {
+ base.OnEnable(); // lets the base mark the editor enabled/inert (OnDisable symmetry)
+ return;
+ }
+
m_MaterialLocation = serializedObject.FindProperty("m_MaterialLocation");
m_Materials = serializedObject.FindProperty("m_Materials");
diff --git a/Editor/Mono/ImportSettings/TextureImporterInspector.cs b/Editor/Mono/ImportSettings/TextureImporterInspector.cs
index 1181422d96..61fb0a4242 100644
--- a/Editor/Mono/ImportSettings/TextureImporterInspector.cs
+++ b/Editor/Mono/ImportSettings/TextureImporterInspector.cs
@@ -342,9 +342,9 @@ internal class Styles
public readonly GUIContent spritePixelsPerUnit = EditorGUIUtility.TrTextContent("Pixels Per Unit", "How many pixels in the sprite correspond to one unit in the world.");
public readonly GUIContent spriteExtrude = EditorGUIUtility.TrTextContent("Extrude Edges", "How much empty area to leave around the sprite in the generated mesh.");
- public readonly GUIContent spriteTriangulation = EditorGUIUtility.TrTextContent("Sprite Mesh Triangulation Method", "Use Legacy for old method. And UTess for Delaunary with Subdivision");
- public readonly GUIContent spriteOutline = EditorGUIUtility.TrTextContent("Sprite Outline Detail", "Sprite Outline Detail.");
- public readonly GUIContent spriteSubdivision = EditorGUIUtility.TrTextContent("Sprite Mesh Subdivision", "Refine triangulation.");
+ public readonly GUIContent spriteTriangulation = EditorGUIUtility.TrTextContent("Mesh Triangulation Method", "Use Legacy for old method. And UTess for Delaunary with Subdivision");
+ public readonly GUIContent spriteOutline = EditorGUIUtility.TrTextContent("Outline Detail", "Sprite Outline Detail.");
+ public readonly GUIContent spriteSubdivision = EditorGUIUtility.TrTextContent("Mesh Subdivision", "Refine triangulation.");
public readonly GUIContent spriteMeshType = EditorGUIUtility.TrTextContent("Mesh Type", "Type of sprite mesh to generate.");
public readonly GUIContent spriteAlignment = EditorGUIUtility.TrTextContent("Pivot", "Sprite pivot point in its localspace. May be used for syncing animation frames of different sizes.");
public readonly GUIContent[] spriteAlignmentOptions =
@@ -671,6 +671,8 @@ void InitializeGUI()
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
Initialize();
}
@@ -1269,8 +1271,10 @@ private void SpriteGUI(TextureInspectorGUIElement guiElements)
m_SpriteTessellationMethod.intValue = (int)spriteTessellationMethod;
if (SpriteTessellationMethod.DelaunaySubdivision == spriteTessellationMethod)
{
+ EditorGUI.indentLevel++;
m_SpriteTessellationDetail.floatValue = EditorGUILayout.Slider(s_Styles.spriteOutline, m_SpriteTessellationDetail.floatValue, 0, 1.0f);
m_SpriteGeometrySubdivision.floatValue = EditorGUILayout.Slider(s_Styles.spriteSubdivision, m_SpriteGeometrySubdivision.floatValue, 0, 1.0f);
+ EditorGUI.indentLevel--;
}
}
@@ -1278,29 +1282,26 @@ private void SpriteGUI(TextureInspectorGUIElement guiElements)
ToggleFromInt(m_SpriteGenerateFallbackPhysicsShape, s_Styles.spriteGenerateFallbackPhysicsShape);
EditorGUI.indentLevel--;
- using (new EditorGUI.DisabledScope(targets.Length != 1))
+ if (SpriteUtilityWindow.DoOpenSpriteEditorWindowUI(targets.Length == 1))
{
- if (SpriteUtilityWindow.DoOpenSpriteEditorWindowUI())
+ if (HasModified())
{
- if (HasModified())
- {
- // To ensure Sprite Editor Window to have the latest texture import setting,
- // We must applied those modified values first.
- var dialogText = string.Format(s_Styles.applyAndContinueToSpriteEditor.text, ((TextureImporter)target).assetPath);
- if (EditorUtility.DisplayDialog(s_Styles.unappliedImportSettings.text, dialogText, s_Styles.yes.text, s_Styles.no.text))
- {
- SaveChanges();
- SpriteUtilityWindow.ShowSpriteEditorWindow(this.assetTarget);
-
- // We reimported the asset which destroyed the editor, so we can't keep running the UI here.
- GUIUtility.ExitGUI();
- }
- }
- else
+ // To ensure Sprite Editor Window to have the latest texture import setting,
+ // We must applied those modified values first.
+ var dialogText = string.Format(s_Styles.applyAndContinueToSpriteEditor.text, ((TextureImporter)target).assetPath);
+ if (EditorUtility.DisplayDialog(s_Styles.unappliedImportSettings.text, dialogText, s_Styles.yes.text, s_Styles.no.text))
{
+ SaveChanges();
SpriteUtilityWindow.ShowSpriteEditorWindow(this.assetTarget);
+
+ // We reimported the asset which destroyed the editor, so we can't keep running the UI here.
+ GUIUtility.ExitGUI();
}
}
+ else
+ {
+ SpriteUtilityWindow.ShowSpriteEditorWindow(this.assetTarget);
+ }
}
}
diff --git a/Editor/Mono/Inspector/AnimationClipEditor.cs b/Editor/Mono/Inspector/AnimationClipEditor.cs
index 8d4b9ab449..92508554ef 100644
--- a/Editor/Mono/Inspector/AnimationClipEditor.cs
+++ b/Editor/Mono/Inspector/AnimationClipEditor.cs
@@ -2095,14 +2095,17 @@ public void Draw(Rect window)
{
// Draw body of tooltip
GUIStyle style = (GUIStyle)"AnimationEventTooltip";
- Vector2 size = style.CalcSize(new GUIContent(m_InstantTooltipText));
- Rect rect = new Rect(window.x + m_InstantTooltipPoint.x, window.y + m_InstantTooltipPoint.y, size.x, size.y);
+ using (new SDFStyleScope(style))
+ {
+ Vector2 size = style.CalcSize(new GUIContent(m_InstantTooltipText));
+ Rect rect = new Rect(window.x + m_InstantTooltipPoint.x, window.y + m_InstantTooltipPoint.y, size.x, size.y);
- // Right align tooltip rect if it would otherwise exceed the bounds of the window
- if (rect.xMax > window.width)
- rect.x = window.width - rect.width;
+ // Right align tooltip rect if it would otherwise exceed the bounds of the window
+ if (rect.xMax > window.width)
+ rect.x = window.width - rect.width;
- GUI.Label(rect, m_InstantTooltipText, style);
+ GUI.Label(rect, m_InstantTooltipText, style);
+ }
}
}
diff --git a/Editor/Mono/Inspector/AssemblyDefinitionImporterInspector.cs b/Editor/Mono/Inspector/AssemblyDefinitionImporterInspector.cs
index 64eb126c2b..e6ecec4e3e 100644
--- a/Editor/Mono/Inspector/AssemblyDefinitionImporterInspector.cs
+++ b/Editor/Mono/Inspector/AssemblyDefinitionImporterInspector.cs
@@ -176,6 +176,8 @@ public static string UnityVersionTypeName
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
//Ensure UIElements handles the IMGUI container with margins
alwaysAllowExpansion = true;
diff --git a/Editor/Mono/Inspector/AssemblyDefinitionReferenceImporterInspector.cs b/Editor/Mono/Inspector/AssemblyDefinitionReferenceImporterInspector.cs
index a7494bca17..bc2d7f7b93 100644
--- a/Editor/Mono/Inspector/AssemblyDefinitionReferenceImporterInspector.cs
+++ b/Editor/Mono/Inspector/AssemblyDefinitionReferenceImporterInspector.cs
@@ -49,6 +49,8 @@ public string path
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
//Ensure UIElements handles the IMGUI container with margins
alwaysAllowExpansion = true;
diff --git a/Editor/Mono/Inspector/AudioManagerInspector.cs b/Editor/Mono/Inspector/AudioManagerInspector.cs
index 787fdd7a24..f8336edced 100644
--- a/Editor/Mono/Inspector/AudioManagerInspector.cs
+++ b/Editor/Mono/Inspector/AudioManagerInspector.cs
@@ -21,7 +21,7 @@ private class Styles
public static GUIContent DefaultSpeakerMode = EditorGUIUtility.TrTextContent("Default Speaker Mode", "Speaker mode at start of the game. This may be changed at runtime using the AudioSettings.Reset function.");
public static GUIContent SampleRate = EditorGUIUtility.TrTextContent("System Sample Rate", "Sample rate at which the output device of the audio system runs. Individual sounds may run at different sample rates and will be slowed down/sped up accordingly to match the output rate.");
public static GUIContent DSPBufferSize = EditorGUIUtility.TrTextContent("DSP Buffer Size", "Length of mixing buffer. This determines the output latency of the game.");
- public static GUIContent AudioFoundation = EditorGUIUtility.TrTextContent("Audio Foundation", "Low-level, platform audio layer. Classic is the same, mature platform layer from previous versions of Unity. Enhanced is the new platform audio layer and is supported on Windows, macOS, iOS, Android (8.1 and later), and Xbox. The benefits include asynchronous starting and stopping of devices and greater control over audio engine behavior. On platforms that don't have enhanced mode yet, the engine will fall back to using classic mode.");
+ public static GUIContent AudioFoundation = EditorGUIUtility.TrTextContent("Audio Foundation", "Low-level, platform audio layer. Classic is the same, mature platform layer from previous versions of Unity. Enhanced is the new platform audio layer and is supported on Windows, macOS, Linux, iOS, Android (8.1 and later), XBOX Series X|S, XBOX One, and Nintendo Switch™. The benefits include asynchronous starting and stopping of devices and greater control over audio engine behavior. On platforms that don't have enhanced mode yet, the engine will fall back to using classic mode.");
public static GUIContent OutputChannelLayout = EditorGUIUtility.TrTextContent("Output Channel Layout", "The audio engine will always run at the selected channel layout and up-mixing or down-mixing will occur to match the device's native channel layout. Alternatively, if DeviceNative is selected, the engine will always run at the device's native channel count, and will be reset if that native channel count changes (i.e. when the default device changes).");
public static GUIContent OutputSamplingRate = EditorGUIUtility.TrTextContent("Output Sampling Rate", "The audio engine will always run at the selected sampling rate and sample-rate conversion will occur to match the device's native sampling rate. Alternatively, if DeviceNative is selected, the engine will always run at the device's native sampling rate, and will be reset if that native sampling rate changes (i.e. when the default device changes).");
public static GUIContent VirtualVoiceCount = EditorGUIUtility.TrTextContent("Max Virtual Voices", "Maximum number of sounds managed by the system. Even though at most RealVoiceCount of the loudest sounds will be physically playing, the remaining sounds will still be updating their play position.");
@@ -37,7 +37,8 @@ private class Styles
private class StylesNonSearchable
{
public static GUIContent DSPBufferSizeInfo = EditorGUIUtility.TrTextContent("The requested buffer size ({0}) has been overridden to {1} by the operating system");
- public static GUIContent EnhancedAudioFoundationInfo = EditorGUIUtility.TrTextContent("Enhanced will be used on Windows, macOS, iOS, Android (8.1 and later), and Xbox. Other platforms will use Classic.");
+ public static GUIContent EnhancedAudioFoundationInfo = EditorGUIUtility.TrTextContent("Enhanced will be used on Windows, macOS, Linux, iOS, Android (8.1 and later), XBOX Series X|S, XBOX One, and Nintendo Switch™. Other platforms will use Classic.");
+ public static GUIContent PassthroughChannelLayoutInfo = EditorGUIUtility.TrTextContent("On Meta Quest devices, this layout is sent directly to the OS hardware spatialization instead of being down-mixed.");
}
static readonly string[] k_AdditionalSearchKeywords =
@@ -131,6 +132,8 @@ public override void OnInspectorGUI()
EditorGUILayout.HelpBox(StylesNonSearchable.EnhancedAudioFoundationInfo.text, MessageType.Info);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_OutputChannelLayout, Styles.OutputChannelLayout);
+ if (m_OutputChannelLayout.intValue == (int)ChannelLayoutBehavior.Surround_7_1_4)
+ EditorGUILayout.HelpBox(StylesNonSearchable.PassthroughChannelLayoutInfo.text, MessageType.Info);
EditorGUILayout.PropertyField(m_OutputSamplingRate, Styles.OutputSamplingRate);
EditorGUI.indentLevel--;
}
diff --git a/Editor/Mono/Inspector/Avatar/AvatarEditor.cs b/Editor/Mono/Inspector/Avatar/AvatarEditor.cs
index f0967b30cc..884e810294 100644
--- a/Editor/Mono/Inspector/Avatar/AvatarEditor.cs
+++ b/Editor/Mono/Inspector/Avatar/AvatarEditor.cs
@@ -135,6 +135,7 @@ protected void ApplyRevertGUI()
if (GUILayout.Button("Apply"))
{
ApplyAndImport();
+ GUIUtility.ExitGUI();
}
}
diff --git a/Editor/Mono/Inspector/Core/AddComponent/AddComponentDataSource.cs b/Editor/Mono/Inspector/Core/AddComponent/AddComponentDataSource.cs
index 39e9be758d..c6c3fbbd81 100644
--- a/Editor/Mono/Inspector/Core/AddComponent/AddComponentDataSource.cs
+++ b/Editor/Mono/Inspector/Core/AddComponent/AddComponentDataSource.cs
@@ -17,6 +17,7 @@ internal class AddComponentDataSource : AdvancedDropdownDataSource
UnityEngine.GameObject[] m_Targets;
internal static readonly string kScriptHeader = "Component/Scripts/";
+ internal static readonly string kNewScriptGroupName = "New script";
public AddComponentDataSource(AdvancedDropdownState state, UnityEngine.GameObject[] targets)
{
@@ -86,13 +87,23 @@ protected AdvancedDropdownItem RebuildTree()
}
}
root = root.childList.Single();
- var newScript = new ComponentDropdownItem("New script", L10n.Tr("New script"));
- newScript.AddChild(new NewScriptDropdownItem());
- root.AddChild(newScript);
+ AddNewScriptGroup(root);
DictionaryPool, int>.Release(pathHashCodeMap);
return root;
}
+ void AddNewScriptGroup(AdvancedDropdownItem parent, string className = null)
+ {
+ var newScriptGroup = new ComponentDropdownItem(kNewScriptGroupName, L10n.Tr("New script"));
+ var newScript = new NewScriptDropdownItem();
+ if (className != null)
+ newScript.className = className;
+ newScriptGroup.AddChild(newScript);
+ parent.AddChild(newScriptGroup);
+ // Seed after AddChild: AddChild assigns the group's final id and the state is keyed by id.
+ m_State.SetSelectedIndex(newScriptGroup, 0);
+ }
+
static List GetSortedMenuItems(UnityEngine.GameObject[] targets)
{
var menus = Unsupported.GetSubmenus("Component");
@@ -184,15 +195,7 @@ protected override AdvancedDropdownItem Search(string searchString)
{
searchTree.AddChild(element);
}
- if (searchTree != null)
- {
- var addNewScriptGroup = new ComponentDropdownItem("New script", L10n.Tr("New script"));
- m_State.SetSelectedIndex(addNewScriptGroup, 0);
- var addNewScript = new NewScriptDropdownItem();
- addNewScript.className = searchString;
- addNewScriptGroup.AddChild(addNewScript);
- searchTree.AddChild(addNewScriptGroup);
- }
+ AddNewScriptGroup(searchTree, searchString);
return searchTree;
}
}
diff --git a/Editor/Mono/Inspector/Core/AddComponent/AddComponentGUI.cs b/Editor/Mono/Inspector/Core/AddComponent/AddComponentGUI.cs
index b82002d7c6..5c0745a1fc 100644
--- a/Editor/Mono/Inspector/Core/AddComponent/AddComponentGUI.cs
+++ b/Editor/Mono/Inspector/Core/AddComponent/AddComponentGUI.cs
@@ -12,7 +12,7 @@ internal class AddComponentGUI : AdvancedDropdownGUI
{
private static class Styles
{
- public static GUIStyle itemStyle = "DD LargeItemStyle";
+ public static readonly GUIStyle itemStyle = "DD LargeItemStyle";
const string k_includeNamespaceProSkin = "{0} ({1}) ";
const string k_includeNamespace = "{0} ({1}) ";
diff --git a/Editor/Mono/Inspector/Core/AddComponent/AddComponentWindow.cs b/Editor/Mono/Inspector/Core/AddComponent/AddComponentWindow.cs
index a6fccbc00c..07fc2f6610 100644
--- a/Editor/Mono/Inspector/Core/AddComponent/AddComponentWindow.cs
+++ b/Editor/Mono/Inspector/Core/AddComponent/AddComponentWindow.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Scripting;
@@ -26,6 +27,7 @@ internal class AnalyticsEventData
private DateTime m_ComponentOpenTime;
private const string kComponentSearch = "ComponentSearchString";
private const int kMaxWindowHeight = 395 - 80;
+ [NoAutoStaticsCleanup] // dropdown UI state intentionally persisted across reloads
private static AdvancedDropdownState s_State = new AdvancedDropdownState();
protected override bool setInitialSelectionPosition { get; } = false;
diff --git a/Editor/Mono/Inspector/Core/AddComponent/NewScriptDropdownItem.cs b/Editor/Mono/Inspector/Core/AddComponent/NewScriptDropdownItem.cs
index 3d2edd1ade..8d7bcd2ab2 100644
--- a/Editor/Mono/Inspector/Core/AddComponent/NewScriptDropdownItem.cs
+++ b/Editor/Mono/Inspector/Core/AddComponent/NewScriptDropdownItem.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEditorInternal;
using Microsoft.CSharp;
@@ -18,6 +19,7 @@ class NewScriptDropdownItem : ComponentDropdownItem
private readonly char[] kInvalidPathChars = new char[] {'<', '>', ':', '"', '|', '?', '*', (char)0};
private readonly char[] kPathSepChars = new char[] {'/', '\\'};
+ [NoAutoStaticsCleanup] // lazy provider, safe to persist; re-created on first use if null
private static System.CodeDom.Compiler.CodeDomProvider s_CSharpDOMProvider;
private string m_Directory = string.Empty;
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownGUI.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownGUI.cs
index 478faec043..f1db6ecb19 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownGUI.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownGUI.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.StyleSheets;
using UnityEngine;
using Event = UnityEngine.Event;
@@ -15,22 +16,26 @@ internal class AdvancedDropdownGUI
{
private static class Styles
{
- public static GUIStyle itemStyle = "DD ItemStyle";
- public static GUIStyle header = "DD HeaderStyle";
+ public static readonly GUIStyle itemStyle = "DD ItemStyle";
+ public static readonly GUIStyle header = "DD HeaderStyle";
+ [NoAutoStaticsCleanup] // mutated in static ctor (padding/clipping), cannot be readonly
public static GUIStyle headerEllipsis = "DD HeaderStyle";
- public static GUIStyle checkMark = "DD ItemCheckmark";
- public static GUIStyle lineSeparator = "DefaultLineSeparator";
- public static GUIStyle rightArrow = "ArrowNavigationRight";
- public static GUIStyle leftArrow = "ArrowNavigationLeft";
- public static GUIStyle searchFieldStyle = new GUIStyle(EditorStyles.toolbarSearchField)
+ public static readonly GUIStyle checkMark = "DD ItemCheckmark";
+ public static readonly GUIStyle lineSeparator = "DefaultLineSeparator";
+ public static readonly GUIStyle rightArrow = "ArrowNavigationRight";
+ public static readonly GUIStyle leftArrow = "ArrowNavigationLeft";
+ public static readonly GUIStyle searchFieldStyle = new GUIStyle(EditorStyles.toolbarSearchField)
{
margin = new RectOffset(5, 4, 4, 5)
};
- public static SVC searchBackgroundColor = new SVC("--theme-toolbar-background-color", Color.black);
+ [NoAutoStaticsCleanup]
+ public static readonly SVC searchBackgroundColor = new SVC("--theme-toolbar-background-color", Color.black);
+ [NoAutoStaticsCleanup] // lazy GUI styles built from EditorStyles, safe to persist
public static GUIStyle helpBox;
+ [NoAutoStaticsCleanup] // lazy GUI styles built from EditorStyles, safe to persist
public static GUIStyle helpBoxText;
- public static GUIContent checkMarkContent = new GUIContent("✔");
+ public static readonly GUIContent checkMarkContent = new GUIContent("✔");
static Styles()
{
@@ -54,12 +59,17 @@ internal static void LoadStyles()
Debug.Assert(Event.current.type == EventType.Repaint && Styles.itemStyle != null);
}
- public static string k_SearchFieldName = "ComponentSearch";
+ public static readonly string k_SearchFieldName = "ComponentSearch";
//This should ideally match line height
private Vector2 s_IconSize = new Vector2(13, 13);
private AdvancedDropdownDataSource m_DataSource;
+ // Help box layout: icon size and the gap between the icon and the text.
+ // Shared between DrawHelpBox and CalcHelpBoxHeight so the two stay in sync.
+ private const float k_HelpBoxIconSize = 24f;
+ private const float k_HelpBoxIconTextGap = 6f;
+
internal Rect m_SearchRect;
internal Rect m_HeaderRect;
@@ -165,18 +175,14 @@ internal virtual void DrawHelpBox(AdvancedDropdownItem.HelpBoxDropdownItem helpB
// Compute inner rect (respects helpbox padding)
var inner = Styles.helpBox.padding.Remove(rect);
- // Icon settings
- const float iconSize = 24f;
- const float gap = 6f;
-
var icon = EditorGUIUtility.GetHelpIcon(helpBoxItem.type);
// Center icon vertically within the inner rect
- float iconY = inner.y + (inner.height - iconSize) * 0.5f;
- var iconRect = new Rect(inner.x, iconY, iconSize, iconSize);
+ float iconY = inner.y + (inner.height - k_HelpBoxIconSize) * 0.5f;
+ var iconRect = new Rect(inner.x, iconY, k_HelpBoxIconSize, k_HelpBoxIconSize);
// Text rect starts after icon
- float textX = iconRect.xMax + gap;
+ float textX = iconRect.xMax + k_HelpBoxIconTextGap;
var textRect = new Rect(textX, inner.y, inner.xMax - textX, inner.height);
if (Event.current.type != EventType.Repaint)
@@ -192,8 +198,9 @@ internal virtual void DrawHelpBox(AdvancedDropdownItem.HelpBoxDropdownItem helpB
internal float CalcHelpBoxHeight(AdvancedDropdownItem.HelpBoxDropdownItem helpBoxItem)
{
- float contentWidth = EditorGUIUtility.currentViewWidth;
- return Styles.helpBoxText.CalcHeight(new GUIContent(helpBoxItem.message), contentWidth) + Styles.helpBox.padding.vertical;
+ float contentWidth = EditorGUIUtility.currentViewWidth - Styles.helpBox.margin.horizontal;
+ float textWidth = contentWidth - Styles.helpBox.padding.horizontal - k_HelpBoxIconSize - k_HelpBoxIconTextGap;
+ return Mathf.Max(k_HelpBoxIconSize, Styles.helpBoxText.CalcHeight(new GUIContent(helpBoxItem.message), textWidth)) + Styles.helpBox.padding.vertical;
}
internal void DrawHeader(AdvancedDropdownItem group, Action backButtonPressed, bool hasParent)
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownItem.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownItem.cs
index 1c86abec7e..1e08bf342d 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownItem.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownItem.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
namespace UnityEditor.IMGUI.Controls
@@ -94,6 +95,7 @@ internal void AddChildAndKeepId(AdvancedDropdownItem child)
m_Children.Add(child);
}
+ [NoAutoStaticsCleanup] // singleton sentinel used for separator identity check, safe to persist
static readonly AdvancedDropdownItem k_SeparatorItem = new SeparatorDropdownItem();
public AdvancedDropdownItem(string name)
@@ -140,6 +142,12 @@ internal bool IsHelpBox(AdvancedDropdownItem item)
return item is HelpBoxDropdownItem;
}
+ // Separators and help boxes are decorative: they can't be hovered, selected, or clicked.
+ internal bool IsSelectable()
+ {
+ return !IsSeparator() && !IsHelpBox(this);
+ }
+
public override string ToString()
{
return m_Name;
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownState.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownState.cs
index 02f7eb1d37..4844578f2c 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownState.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownState.cs
@@ -47,36 +47,41 @@ private AdvancedDropdownItemState GetStateForItem(AdvancedDropdownItem item)
internal void MoveDownSelection(AdvancedDropdownItem item)
{
- var state = GetStateForItem(item);
- var selectedIndex = state.selectedIndex;
- do
- {
- ++selectedIndex;
- }
- while (selectedIndex < item.childList.Count && item.childList[selectedIndex].IsSeparator());
-
- if (selectedIndex >= item.childList.Count)
- selectedIndex = 0;
-
- if (selectedIndex < item.childList.Count)
- SetSelectionOnItem(item, selectedIndex);
+ MoveSelection(item, 1);
}
internal void MoveUpSelection(AdvancedDropdownItem item)
{
- var state = GetStateForItem(item);
- var selectedIndex = state.selectedIndex;
- do
- {
- --selectedIndex;
- }
- while (selectedIndex >= 0 && item.childList[selectedIndex].IsSeparator());
+ MoveSelection(item, -1);
+ }
- if (selectedIndex < 0)
- selectedIndex = item.childList.Count - 1;
+ // Moves the selection to the next selectable child in the given direction
+ // (+1 = down, -1 = up), wrapping around the list and skipping decorative rows
+ // (separators, help boxes). Does nothing if the level has no selectable child.
+ private void MoveSelection(AdvancedDropdownItem item, int direction)
+ {
+ var count = item.childList.Count;
+ if (count == 0)
+ return;
+
+ var index = GetStateForItem(item).selectedIndex;
+
+ // Normalize an empty selection (-1) so the first step lands on the natural
+ // end of the list: the top when moving down, the bottom when moving up.
+ if (index < 0)
+ index = direction > 0 ? -1 : count;
- if (selectedIndex >= 0)
- SetSelectionOnItem(item, selectedIndex);
+ // Scan at most one full loop so the wrap path also skips decorative rows
+ // instead of landing on a leading/trailing separator or help box.
+ for (var step = 0; step < count; step++)
+ {
+ index = (index + direction + count) % count;
+ if (item.childList[index].IsSelectable())
+ {
+ SetSelectionOnItem(item, index);
+ return;
+ }
+ }
}
internal void SetSelectionOnItem(AdvancedDropdownItem item, int selectedIndex)
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownWindow.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownWindow.cs
index 937e1a812e..6eb5ae7d48 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownWindow.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/AdvancedDropdownWindow.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.Callbacks;
using UnityEngine;
using Event = UnityEngine.Event;
@@ -16,8 +17,11 @@ internal class AdvancedDropdownWindow : EditorWindow
{
private static class Styles
{
+ [NoAutoStaticsCleanup] // mutated in static ctor (padding), cannot be readonly
public static GUIStyle background = "DD Background";
+ [NoAutoStaticsCleanup] // mutated in static ctor (padding), cannot be readonly
public static GUIStyle previewHeader = new GUIStyle(EditorStyles.label);
+ [NoAutoStaticsCleanup] // mutated in static ctor (padding), cannot be readonly
public static GUIStyle previewText = new GUIStyle(EditorStyles.wordWrappedLabel);
static Styles()
@@ -53,6 +57,9 @@ static Styles()
private bool m_DirtyList = true;
private bool m_NeedsResize = false;
+ // The dropdown width is computed once when it opens and then kept constant.
+ private float m_FixedWidth;
+
// Caller-supplied size constraints captured in Init() so the dynamic resize logic can
// still respect minimumSize/maximumSize even after they're overwritten by ShowAsDropDown.
private Vector2 m_OriginalMinSize;
@@ -236,6 +243,9 @@ public void Init(Rect buttonRect)
ShowAsDropDown(buttonRect, initialSize, GetLocationPriority());
+ // Remember the width chosen on open so later navigation only ever changes the height.
+ m_FixedWidth = position.width;
+
if (setInitialSelectionPosition)
{
m_InitialSelectionPosition = m_Gui.GetSelectionHeight(m_DataSource, buttonRect);
@@ -422,7 +432,7 @@ private void HandleKeyboard()
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
var selected = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
- if (selected != null)
+ if (selected != null && selected.IsSelectable())
{
if (selected.hasChildren)
{
@@ -541,6 +551,10 @@ private void DrawList(AdvancedDropdownItem item)
if (item != m_CurrentlyRenderedTree)
continue;
+ // Decorative items (separators, help boxes) are not interactive.
+ if (!child.IsSelectable())
+ continue;
+
// Select the element the mouse cursor is over.
// Only do it on mouse move - keyboard controls are allowed to overwrite this until the next time the mouse moves.
if (Event.current.type == EventType.MouseMove || Event.current.type == EventType.MouseDrag)
@@ -647,6 +661,9 @@ private void ResizeWindowForCurrentLevel()
var newSize = CalculateWindowSize(m_ButtonRectScreenPos);
+ // Keep the width fixed at the value chosen when the dropdown opened; recalc height only.
+ newSize.x = m_FixedWidth;
+
// Skip if computed size matches current window size — common for subclasses that pin the window to a fixed size
const float kEpsilon = 0.5f;
if (Mathf.Abs(newSize.x - position.width) < kEpsilon && Mathf.Abs(newSize.y - position.height) < kEpsilon)
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/MultiLevelDataSource.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/MultiLevelDataSource.cs
index 79f250165f..f28c227b6f 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/MultiLevelDataSource.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/MultiLevelDataSource.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor.IMGUI.Controls
{
@@ -21,6 +22,7 @@ internal string label
set { m_Label = value; }
}
+ [NoAutoStaticsCleanup] // selection index for currently open dropdown, safe to persist
private static int m_SelectedIndex;
internal int selectedIndex
{
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/SimpleDataSource.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/SimpleDataSource.cs
index 9249d990a1..805b8f8934 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/SimpleDataSource.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/DataSources/SimpleDataSource.cs
@@ -3,6 +3,7 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System.Linq;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
namespace UnityEditor.IMGUI.Controls
@@ -15,6 +16,7 @@ internal GUIContent[] displayedOptions
set { m_DisplayedOptions = value; }
}
+ [NoAutoStaticsCleanup] // selection index for currently open dropdown, safe to persist
private static int m_SelectedIndex;
#pragma warning disable 0649
private AdvancedDropdownState m_State;
diff --git a/Editor/Mono/Inspector/Core/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs b/Editor/Mono/Inspector/Core/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs
index e62c5d9312..b3070b0231 100644
--- a/Editor/Mono/Inspector/Core/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs
+++ b/Editor/Mono/Inspector/Core/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs
@@ -3,11 +3,13 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace UnityEditor
{
+ [AutoStaticsCleanupOnCodeReload]
internal static partial class StatelessAdvancedDropdown
{
private static AdvancedDropdownWindow s_Instance;
diff --git a/Editor/Mono/Inspector/Core/GenericInspector.cs b/Editor/Mono/Inspector/Core/GenericInspector.cs
index 99f630d8ec..b09f7cd579 100644
--- a/Editor/Mono/Inspector/Core/GenericInspector.cs
+++ b/Editor/Mono/Inspector/Core/GenericInspector.cs
@@ -24,9 +24,9 @@ private enum OptimizedBlockState
static class Styles
{
- public static string missingScriptMessage = L10n.Tr("The associated script can not be loaded: {}\n\nThis could be because the script has a compile error or because the script was deleted.\nFix any compile errors if present or assign a valid script.");
- public static string missingScriptMessageForPrefabInstance = L10n.Tr("The associated script can not be loaded: {}\n\nYou must resolve missing scripts on prefabs on the prefab asset itself through the prefab view. Open the source prefab asset for this prefab instance to continue.");
- public static string missingSerializeReferenceInstanceMessage = L10n.Tr("This object contains SerializeReference types which are missing.\nFor more information see SerializationUtility.HasManagedReferencesWithMissingTypes.");
+ public static readonly string missingScriptMessage = L10n.Tr("The associated script can not be loaded: {}\n\nThis could be because the script has a compile error or because the script was deleted.\nFix any compile errors if present or assign a valid script.");
+ public static readonly string missingScriptMessageForPrefabInstance = L10n.Tr("The associated script can not be loaded: {}\n\nYou must resolve missing scripts on prefabs on the prefab asset itself through the prefab view. Open the source prefab asset for this prefab instance to continue.");
+ public static readonly string missingSerializeReferenceInstanceMessage = L10n.Tr("This object contains SerializeReference types which are missing.\nFor more information see SerializationUtility.HasManagedReferencesWithMissingTypes.");
}
internal static string GetMissingSerializeRefererenceMessageContainer()
diff --git a/Editor/Mono/Inspector/Core/InspectorWindow.cs b/Editor/Mono/Inspector/Core/InspectorWindow.cs
index 1de104ce65..9e29b85e1f 100644
--- a/Editor/Mono/Inspector/Core/InspectorWindow.cs
+++ b/Editor/Mono/Inspector/Core/InspectorWindow.cs
@@ -4,9 +4,9 @@
using System;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.Bindings;
-using UnityEngine.Pool;
using UnityEngine.Scripting;
using UnityEngine.UIElements;
@@ -16,13 +16,15 @@ namespace UnityEditor
{
[VisibleToOtherModules("UnityEditor.PlayModeModule", "UnityEditor.UIToolkitAuthoringModule")]
[EditorWindowTitle(title = k_InspectorWindowTitle, useTypeNameAsIconName = true)]
- internal class InspectorWindow : PropertyEditor, IPropertyView, IHasCustomMenu
+ internal partial class InspectorWindow : PropertyEditor, IPropertyView, IHasCustomMenu
{
const string k_InspectorWindowTitle = "Inspector";
const string k_InspectorWindowTitleDebug = "Inspector (Debug)";
const string k_InspectorWindowTitleDebugInternal = "Inspector (Debug Internal)";
- static readonly List m_AllInspectors = new List();
+ [AutoStaticsCleanupOnCodeReload]
+ static List m_AllInspectors = new List();
+ [NoAutoStaticsCleanup] // bool flag, safe to persist; rebuild triggered on next repaint
static bool s_AllOptimizedGUIBlocksNeedsRebuild;
[SerializeField] EditorGUIUtility.EditorLockTrackerWithActiveEditorTracker m_LockTracker = new EditorGUIUtility.EditorLockTrackerWithActiveEditorTracker();
@@ -251,6 +253,9 @@ protected override void Update()
}
}
+ [NoAutoStaticsCleanup]
+ static List s_WindowsSnapshot = new List(32);
+
[UsedByNativeCode]
internal static void RedrawFromNative()
{
@@ -260,15 +265,28 @@ internal static void RedrawFromNative()
// Acquire a snapshot instead of directly iterating over activeEditorWindows as calling
// RebuildContentsContainers can mutate activeEditorWindows.
- var activeWindowCount = activeEditorWindows.Count;
- using var windowsSnapshot = new RentSpan(activeWindowCount);
- for (int i = 0; i < activeWindowCount; ++i)
- windowsSnapshot.Span[i] = activeEditorWindows[i];
+ var snapshot = s_WindowsSnapshot;
+
+ // Guard against re-entry by setting s_WindowsSnapshot to null for the current entry (the finally statement will restore it).
+ if (snapshot != null)
+ s_WindowsSnapshot = null;
+ else // If snapshot is null, we're re-entering. We shouldn't overwrite s_WindowsSnapshot therefore fallback to allocating new.
+ snapshot = new List(32);
+
+ try
+ {
+ snapshot.AddRange(activeEditorWindows);
- foreach (var editorWindow in windowsSnapshot)
+ for (int i = 0; i < snapshot.Count; ++i)
+ {
+ if (snapshot[i] is PropertyEditor propertyEditor && propertyEditor != null)
+ propertyEditor.RebuildContentsContainers();
+ }
+ }
+ finally
{
- if (editorWindow != null && editorWindow is PropertyEditor propertyEditor)
- propertyEditor.RebuildContentsContainers();
+ snapshot.Clear();
+ s_WindowsSnapshot = snapshot;
}
}
diff --git a/Editor/Mono/Inspector/Core/PropertyEditor.cs b/Editor/Mono/Inspector/Core/PropertyEditor.cs
index 79ea836c93..2e09e89fd5 100644
--- a/Editor/Mono/Inspector/Core/PropertyEditor.cs
+++ b/Editor/Mono/Inspector/Core/PropertyEditor.cs
@@ -23,6 +23,7 @@
using JetBrains.Annotations;
using Unity.Collections;
using Unity.Profiling;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.UIElements;
using UnityEngine.Pool;
using Button = UnityEngine.UIElements.Button;
@@ -50,7 +51,7 @@ interface IPropertySourceOpener
Object hoveredObject { get; }
}
- class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
+ partial class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
{
internal const string k_AssetPropertiesMenuItemName = "Assets/Properties... _&P";
protected const string s_MultiEditClassName = "unity-inspector-no-multi-edit-warning";
@@ -77,7 +78,6 @@ class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
protected const int k_AutoScrollZoneHeight = 24;
const float m_PreviewDefaultHeight = 200;
const float m_PreviewMinHeight = 20;
-
static readonly string k_DebugInfoPanelTooltip = L10n.Tr("In Debug mode, the Inspector also displays the item's private properties and doesn't use custom inspector code.");
static readonly string k_ExitDebugButtonTooltip = L10n.Tr("Change the Inspector window back to Normal mode.");
static readonly string k_DebugModeLabel = L10n.Tr("Inspector mode: Debug");
@@ -105,14 +105,15 @@ class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
[SerializeField] protected List m_EntityIdsLockedBeforeSerialization = new List();
[SerializeField] protected PreviewResizer m_PreviewResizer = new PreviewResizer();
protected LabelGUI m_LabelGUI = new LabelGUI();
- [SerializeField] protected EntityId m_LastInspectedObjectEntityId = EntityId.None;
[SerializeField] protected float m_LastVerticalScrollValue = 0;
[SerializeField] protected string m_GlobalObjectId = "";
[SerializeField] protected InspectorMode m_InspectorMode = InspectorMode.Normal;
- private static readonly List m_AllPropertyEditors = new List();
+ [AutoStaticsCleanupOnCodeReload]
+ private static List m_AllPropertyEditors = new List();
private Object m_InspectedObject;
private string m_ExpectedTitle;
+ [AutoStaticsCleanupOnCodeReload]
private static PropertyEditor s_LastPropertyEditor;
protected EntityId m_LastInitialEditorEntityId;
protected Component[] m_ComponentsInPrefabSource;
@@ -140,6 +141,7 @@ class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
protected VisualElement previewAndLabelElement => m_PreviewAndLabelElement ?? (m_PreviewAndLabelElement = FindVisualElementInTreeByClassName(s_FooterInfoClassName));
protected VisualElement m_VersionControlElement;
protected VisualElement versionControlElement => m_VersionControlElement ?? (m_VersionControlElement = FindVisualElementInTreeByClassName(s_HeaderInfoClassName));
+ [AutoStaticsCleanupOnCodeReload]
protected static Dictionary m_VersionControlBarState = new Dictionary();
protected VisualElement m_MultiEditLabel;
protected ScrollView m_ScrollView;
@@ -156,13 +158,16 @@ class PropertyEditor : EditorWindow, IPropertyView, IHasCustomMenu
Button m_ExitDebugModeButton;
List m_SupportedDataModes = new(4);
+ [NoAutoStaticsCleanup] // fixed singleton list of disabled modes, safe to persist
static readonly List k_DisabledDataModes = new() {DataMode.Disabled};
public GUIView parent => m_Parent;
public HashSet editorsWithImportedObjectLabel { get; } = new HashSet();
public EditorDragging editorDragging { get; }
public Editor lastInteractedEditor { get; set; }
+ [AutoStaticsCleanupOnCodeReload]
internal static PropertyEditor HoveredPropertyEditor { get; private set; }
+ [AutoStaticsCleanupOnCodeReload]
internal static PropertyEditor FocusedPropertyEditor { get; private set; }
EditorElementUpdater m_EditorElementUpdater;
@@ -208,7 +213,7 @@ protected Rect bottomAreaDropRectangle
internal Rect scrollViewportRect => m_ScrollView.contentViewport.rect;
- protected static class Styles
+ protected static partial class Styles
{
public static readonly GUIStyle preToolbar = "preToolbar";
public static readonly GUIStyle preToolbar2 = "preToolbar2";
@@ -220,15 +225,15 @@ protected static class Styles
public static readonly GUIContent preTitle = EditorGUIUtility.TrTextContent("Preview");
public static readonly GUIContent labelTitle = EditorGUIUtility.TrTextContent("Asset Labels");
public static readonly GUIContent addComponentLabel = EditorGUIUtility.TrTextContent("Add Component");
- public static GUIStyle preBackground = "preBackground";
- public static GUIStyle footer = "IN Footer";
- public static GUIStyle preMargins = new GUIStyle() {margin = new RectOffset(0, 0, 0, 4)};
- public static GUIStyle preOptionsButton = new GUIStyle(EditorStyles.toolbarButtonRight) { padding = new RectOffset(), contentOffset = new Vector2(1, 0) };
- public static GUIStyle addComponentArea = EditorStyles.inspectorTitlebar;
- public static GUIStyle addComponentButtonStyle = "AC Button";
+ public static readonly GUIStyle preBackground = "preBackground";
+ public static readonly GUIStyle footer = "IN Footer";
+ public static readonly GUIStyle preMargins = new GUIStyle() {margin = new RectOffset(0, 0, 0, 4)};
+ public static readonly GUIStyle preOptionsButton = new GUIStyle(EditorStyles.toolbarButtonRight) { padding = new RectOffset(), contentOffset = new Vector2(1, 0) };
+ public static readonly GUIStyle addComponentArea = EditorStyles.inspectorTitlebar;
+ public static readonly GUIStyle addComponentButtonStyle = "AC Button";
public static readonly GUIContent menuIcon = EditorGUIUtility.TrIconContent("_Menu");
- public static GUIStyle previewMiniLabel = EditorStyles.whiteMiniLabel;
- public static GUIStyle typeSelection = "IN TypeSelection";
+ public static readonly GUIStyle previewMiniLabel = EditorStyles.whiteMiniLabel;
+ public static readonly GUIStyle typeSelection = "IN TypeSelection";
public static readonly GUIContent vcsCheckoutHint = EditorGUIUtility.TrTextContent("Under Version Control\nCheck out this asset in order to make changes.", EditorGUIUtility.GetHelpIcon(MessageType.Info));
public static readonly GUIContent vcsNotConnected = EditorGUIUtility.TrTextContent("VCS ({0}) is not connected");
@@ -242,11 +247,15 @@ protected static class Styles
public static readonly GUIContent vcsSubmit = EditorGUIUtility.TrTextContent("Submit");
public static readonly GUIContent vcsRevert = EditorGUIUtility.TrTextContent("Revert");
public static readonly GUIContent vcsRevertUnchanged = EditorGUIUtility.TrTextContent("Revert Unchanged");
+ [NoAutoStaticsCleanup] // array of auto-exempt GUIContent refs, safe to persist
public static readonly GUIContent[] vcsRevertMenuNames = {vcsRevertUnchanged};
+ [NoAutoStaticsCleanup] // ok the static method assigned here is stateless
public static readonly GenericMenu.MenuFunction2[] vcsRevertMenuActions = {DoRevertUnchanged};
public static readonly GUIStyle vcsButtonStyle = EditorStyles.miniButton;
+ [NoAutoStaticsCleanup] // GUIStyle persists safely; static constructor applies modifiers on first access
public static GUIStyle vcsRevertStyle = new GUIStyle(EditorStyles.dropDownList);
public static readonly GUIStyle vcsBarStyleOneRow = EditorStyles.toolbar;
+ [NoAutoStaticsCleanup] // GUIStyle persists safely; static constructor applies modifiers on first access
public static GUIStyle vcsBarStyleTwoRows = new GUIStyle(EditorStyles.toolbar);
public static readonly string objectDisabledModuleWarningFormat = L10n.Tr(
"The built-in package '{0}', which implements this component type, has been disabled in Package Manager. This object will be removed in play mode and from any builds you make."
@@ -255,8 +264,10 @@ protected static class Styles
"The built-in package '{0}', which is required by the package '{1}', which implements this component type, has been disabled in Package Manager. This object will be removed in play mode and from any builds you make."
);
- public static SVC lineSeparatorOffset = new SVC("AC-Button", "--separator-line-top-offset");
- public static SVC lineSeparatorColor = new SVC("--theme-line-separator-color", Color.red);
+ [NoAutoStaticsCleanup]
+ public static readonly SVC lineSeparatorOffset = new SVC("AC-Button", "--separator-line-top-offset");
+ [NoAutoStaticsCleanup]
+ public static readonly SVC lineSeparatorColor = new SVC("--theme-line-separator-color", Color.red);
static Styles()
{
@@ -428,9 +439,9 @@ protected virtual void OnDisable()
ClearPreviewables();
- // save vertical scroll position
- m_LastInspectedObjectEntityId = GetInspectedObject()?.GetEntityId() ?? EntityId.None;
- m_LastVerticalScrollValue = m_ScrollView?.verticalScroller.value ?? 0;
+ // Persist the live scroll offset so it survives a domain reload.
+ if (m_ScrollView != null)
+ m_LastVerticalScrollValue = m_ScrollView.verticalScroller.value;
EditorApplication.focusChanged -= OnFocusChanged;
Undo.undoRedoEvent -= OnUndoRedoPerformed;
@@ -526,7 +537,8 @@ protected virtual void OnGUI()
m_HasPreviewPeriodicCheckDelayer?.Execute();
}
- static readonly List s_Editors = new List();
+ [AutoStaticsCleanupOnCodeReload]
+ static List s_Editors = new List();
[UsedImplicitly]
protected virtual void Update()
@@ -661,7 +673,6 @@ private void OnGeometryChanged(GeometryChangedEvent e)
m_PreviewResizer.SetExpanded(false);
}
}
- RestoreVerticalScrollIfNeeded();
}
internal static void ClearAndRebuildAll()
@@ -1171,6 +1182,12 @@ protected virtual void BeginRebuildContentContainers() {}
protected virtual void EndRebuildContentContainers() {}
internal virtual void RebuildContentsContainers()
{
+ // Capture the live scroll offset before teardown so it survives the rebuild. Skip on the
+ // first rebuild after a domain reload, where the ScrollView is freshly at 0 and the
+ // serialized value is authoritative.
+ if (!m_FirstInitialize && m_ScrollView != null)
+ m_LastVerticalScrollValue = m_ScrollView.verticalScroller.value;
+
ClearPreviewables();
m_TypeSelectionList = null;
m_FirstInitialize = false;
@@ -1294,11 +1311,23 @@ internal virtual void RebuildContentsContainers()
}
k_CreateInspectorElements.End();
+ // When restoring scroll, build just enough editors for the saved offset to stay reachable, so it can be
+ // re-applied below without the scroller clamping it to 0. The remaining editors keep building time-sliced.
+ if (m_LastVerticalScrollValue > 0f && editorsElement != null && m_ScrollView != null)
+ {
+ m_EditorElementUpdater.CreateInspectorElementsToReachScrollOffset(m_ScrollView, editorsElement, m_LastVerticalScrollValue);
+ }
+
rootVisualElement.MarkDirtyRepaint();
ScriptAttributeUtility.ClearGlobalCache();
EndRebuildContentContainers();
+
+ // Re-apply the preserved scroll offset now that the content height is final.
+ if (m_LastVerticalScrollValue > 0f && m_ScrollView != null)
+ m_ScrollView.verticalScroller.value = m_LastVerticalScrollValue;
+
Repaint();
RefreshTitle();
}
@@ -2475,16 +2504,6 @@ private void DrawEditors(Editor[] editors)
}
}
- private void RestoreVerticalScrollIfNeeded()
- {
- if (m_LastInspectedObjectEntityId == EntityId.None)
- return;
- var inspectedObjectInstanceID = GetInspectedObject()?.GetEntityId() ?? EntityId.None;
- if (inspectedObjectInstanceID == m_LastInspectedObjectEntityId && inspectedObjectInstanceID != EntityId.None)
- m_ScrollView.verticalScroller.value = m_LastVerticalScrollValue;
- m_LastInspectedObjectEntityId = EntityId.None; // reset to make sure the restore occurs once
- }
-
void OnPrefabInstanceUnpacked(GameObject unpackedPrefabInstance, PrefabUnpackMode unpackMode)
{
if (m_RemovedComponents == null)
diff --git a/Editor/Mono/Inspector/Core/RootEditor.cs b/Editor/Mono/Inspector/Core/RootEditor.cs
index 72ced64b63..6672873262 100644
--- a/Editor/Mono/Inspector/Core/RootEditor.cs
+++ b/Editor/Mono/Inspector/Core/RootEditor.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Internal;
using UnityEngine.Pool;
using UnityEngine.Scripting;
@@ -41,7 +42,8 @@ private static System.Type signature(UnityEngine.Object[] objects, UnityEngine.O
}
}
- internal static class RootEditorUtils
+ [AutoStaticsCleanupOnCodeReload]
+ internal static partial class RootEditorUtils
{
class RootEditorDesc
{
@@ -54,9 +56,10 @@ class RootEditorDesc
}
private static bool s_SuppressRootEditor = false;
- private static readonly List kSRootEditor = new List();
+ private static List kSRootEditor = new List();
- static RootEditorUtils()
+ [OnCodeLoaded]
+ static void Initialize()
{
Rebuild();
}
diff --git a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceDrawer.cs b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceDrawer.cs
index ff47cdd0c6..a6823b1fa4 100644
--- a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceDrawer.cs
+++ b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceDrawer.cs
@@ -12,11 +12,11 @@
abstract class BaseExposedPropertyDrawer : UnityEditor.PropertyDrawer
{
- private static float kDriveWidgetWidth = 18.0f;
- private static GUIStyle kDropDownStyle = null;
- private static Color kMissingOverrideColor = new Color(1.0f, 0.11f, 0.11f, 1.0f);
- protected static string kSetExposedPropertyMsg = "Set Exposed Property";
- protected static string kClearExposedPropertyMsg = "Clear Exposed Property";
+ private static readonly float kDriveWidgetWidth = 18.0f;
+ private static readonly GUIStyle kDropDownStyle = "ShurikenDropdown";
+ private static readonly Color kMissingOverrideColor = new Color(1.0f, 0.11f, 0.11f, 1.0f);
+ protected static readonly string kSetExposedPropertyMsg = "Set Exposed Property";
+ protected static readonly string kClearExposedPropertyMsg = "Clear Exposed Property";
internal const string kVisualElementName = "ExposedReference";
internal readonly GUIContent ExposePropertyContent = EditorGUIUtility.TrTextContent("Expose Property");
@@ -44,8 +44,6 @@ internal enum OverrideState
public BaseExposedPropertyDrawer()
{
- if (kDropDownStyle == null)
- kDropDownStyle = "ShurikenDropdown";
}
static internal ExposedPropertyMode GetExposedPropertyMode(string propertyName)
diff --git a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/PropertyDrawers.cs b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/PropertyDrawers.cs
index 40f7014426..22477829d5 100644
--- a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/PropertyDrawers.cs
+++ b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/PropertyDrawers.cs
@@ -17,7 +17,7 @@ namespace UnityEditor
[CustomPropertyDrawer(typeof(RangeAttribute))]
internal sealed class RangeDrawer : PropertyDrawer
{
- private static string s_InvalidTypeMessage = L10n.Tr("Use Range with float or int.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use Range with float or int.");
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
@@ -58,7 +58,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property)
[CustomPropertyDrawer(typeof(MinAttribute))]
internal sealed class MinDrawer : PropertyDrawer
{
- private static string s_InvalidTypeMessage = L10n.Tr("Use Min with float, int or Vector.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use Min with float, int or Vector.");
private MinAttribute minAttribute
{
@@ -266,7 +266,7 @@ private EntityId OnValidateValue(EntityId value)
[CustomPropertyDrawer(typeof(MultilineAttribute))]
internal sealed class MultilineDrawer : PropertyDrawer
{
- private static string s_InvalidTypeMessage = L10n.Tr("Use Multiline with string.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use Multiline with string.");
private const int kLineHeight = 13;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
@@ -319,7 +319,7 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent
internal sealed class TextAreaDrawer : PropertyDrawer
{
private const int kLineHeight = 15;
- private static string s_InvalidTypeMessage = L10n.Tr("Use TextAreaDrawer with string.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use TextAreaDrawer with string.");
private Vector2 m_ScrollPosition = new Vector2();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
@@ -399,7 +399,7 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent
[CustomPropertyDrawer(typeof(ColorUsageAttribute))]
internal sealed class ColorUsageDrawer : PropertyDrawer
{
- private static string s_InvalidTypeMessage = L10n.Tr("Use ColorUsageDrawer with color.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use ColorUsageDrawer with color.");
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
@@ -441,7 +441,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property)
[CustomPropertyDrawer(typeof(GradientUsageAttribute))]
internal sealed class GradientUsageDrawer : PropertyDrawer
{
- private static string s_InvalidTypeMessage = L10n.Tr("Use GradientUsageDrawer with gradient.");
+ private static readonly string s_InvalidTypeMessage = L10n.Tr("Use GradientUsageDrawer with gradient.");
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
@@ -475,8 +475,8 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property)
[CustomPropertyDrawer(typeof(DelayedAttribute))]
internal sealed class DelayedDrawer : PropertyDrawer
{
- static string s_InvalidTypeMessageIMGUI = L10n.Tr("Use Delayed with float, int or string when using IMGUI.");
- static string s_InvalidTypeMessageUITK = L10n.Tr("Use Delayed with float, int, string, Vector or Rect when using UI Toolkit.");
+ static readonly string s_InvalidTypeMessageIMGUI = L10n.Tr("Use Delayed with float, int or string when using IMGUI.");
+ static readonly string s_InvalidTypeMessageUITK = L10n.Tr("Use Delayed with float, int, string, Vector or Rect when using UI Toolkit.");
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
diff --git a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs
index 0f9c00560d..651eb8f1b6 100644
--- a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs
+++ b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEditorInternal;
using UnityEngine.Bindings;
@@ -15,8 +16,9 @@
namespace UnityEditor
{
[VisibleToOtherModules("UnityEditor.UIBuilderModule")]
- internal class PropertyHandler : IDisposable
+ internal partial class PropertyHandler : IDisposable
{
+ [AutoStaticsCleanupOnCodeReload]
readonly static Dictionary> s_DefaultObjectReferenceCache = new();
List m_PropertyDrawers;
@@ -43,11 +45,15 @@ internal PropertyDrawer propertyDrawer
bool isCurrentlyNested => m_NestingLevel > 0;
+ [AutoStaticsCleanupOnCodeReload]
internal static Dictionary s_reorderableLists = new Dictionary();
+ [NoAutoStaticsCleanup] // transient inspector state counters, safe to persist
static EntityId s_LastInspectionTarget;
+ [NoAutoStaticsCleanup] // transient inspector state counters, safe to persist
static int s_LastInspectorNumComponents;
- static PropertyHandler()
+ [OnCodeLoaded]
+ static void Initialize()
{
Undo.undoRedoEvent += OnUndoRedo;
}
@@ -459,6 +465,7 @@ public void CallMenuCallback(object[] targets, MethodInfo method)
method.Invoke(target, Array.Empty());
}
+ [NoAutoStaticsCleanup] // used for avoiding repeated allocations, cleared after use systematically
static List s_CachedComponents = new List();
internal void TestInvalidateCache()
diff --git a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.cs b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.cs
index 30c98f956f..aacf912b44 100644
--- a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.cs
+++ b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.cs
@@ -7,6 +7,7 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.Collections;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.Bindings;
using UnityEngine.Rendering;
@@ -14,8 +15,10 @@
namespace UnityEditor
{
+ [AutoStaticsCleanupOnCodeReload]
[VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.GraphToolkitModule")]
- internal class ScriptAttributeUtility
+ [AutoStaticsCleanupOnCodeReload]
+ internal partial class ScriptAttributeUtility
{
readonly struct CustomPropertyDrawerContainer
{
@@ -33,18 +36,21 @@ public CustomPropertyDrawerContainer(Type drawerType, Type[] supportedRenderPipe
// Internal API members
internal static Stack s_DrawerStack = new Stack();
+ [NoAutoStaticsCleanup]
private static Dictionary> s_BuiltinAttributes = null;
static Dictionary> s_AutoLoadProperties;
+ [NoAutoStaticsCleanup]
private static PropertyHandler s_SharedNullHandler = new PropertyHandler();
private static PropertyHandler s_NextHandler = new PropertyHandler();
private static PropertyHandlerCache s_GlobalCache = new PropertyHandlerCache();
private static PropertyHandlerCache s_CurrentCache = null;
- static readonly Lazy> k_DrawerTypeForType = new(BuildDrawerTypeForTypeDictionary);
+ static Lazy> k_DrawerTypeForType = new(BuildDrawerTypeForTypeDictionary);
static readonly Dictionary k_DrawerStaticTypesCache = new();
static readonly Dictionary k_SupportedRenderPipelinesForSerializedObject = new();
+ [NoAutoStaticsCleanup]
static readonly Comparer k_RenderPipelineTypeComparer
= Comparer.Create((c1, c2)
=>
@@ -572,7 +578,9 @@ class FieldInfoCache
// Precompiled regexes used by GetFieldInfoFromPropertyPath on cache misses to avoid pattern parsing
// and string concatenation. The trailing-anchor variant is used for the end-of-path check;
// the unanchored variant is used to strip all Array.data[x] segments out of the path.
+ [NoAutoStaticsCleanup]
static readonly Regex k_ArrayDataAtEndRegex = new Regex(@"\.Array\.data\[[0-9]+\]$", RegexOptions.Compiled);
+ [NoAutoStaticsCleanup]
static readonly Regex k_ArrayDataRegex = new Regex(@"\.Array\.data\[[0-9]+\]", RegexOptions.Compiled);
private static FieldInfo GetFieldInfoFromPropertyPath(Type host, string path, out Type type)
diff --git a/Editor/Mono/Inspector/Core/Utils/ObsoleteMessageHelper.cs b/Editor/Mono/Inspector/Core/Utils/ObsoleteMessageHelper.cs
index 3f4d37b837..82b2178bd3 100644
--- a/Editor/Mono/Inspector/Core/Utils/ObsoleteMessageHelper.cs
+++ b/Editor/Mono/Inspector/Core/Utils/ObsoleteMessageHelper.cs
@@ -6,13 +6,15 @@
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor
{
- static class ObsoleteMessageHelper
+ static partial class ObsoleteMessageHelper
{
+ [NoAutoStaticsCleanup] // compiled from a fixed literal pattern with no runtime-state dependency; safe to persist across reload
static Regex s_VersionTagRegex;
// Matches version tags in the format #tagName(MAJOR.MINOR) where tagName is any latin letters.
@@ -50,6 +52,7 @@ public ObsoleteMessageContainer(string message, HelpBoxMessageType messageType,
}
}
+ [AutoStaticsCleanupOnCodeReload] // lazy cache of obsolete type messages, must reset on reload
private static Dictionary s_ObsoleteTypeMessages;
private static Type ResolveReplacementType(string typeName)
@@ -93,6 +96,8 @@ public static bool TryGetObsoleteMessage(Editor editor, out ObsoleteMessageConta
foreach (var type in obsoleteTypes)
{
var attr = type.GetCustomAttribute();
+ if (attr == null)
+ continue; // TODO: attr should never be null, it indicates a TypeCache issue!!!
var message = string.IsNullOrEmpty(attr.Message)
? "This component has been marked as obsolete."
: attr.Message;
diff --git a/Editor/Mono/Inspector/Editor.cs b/Editor/Mono/Inspector/Editor.cs
index c6a17fd216..1776516953 100644
--- a/Editor/Mono/Inspector/Editor.cs
+++ b/Editor/Mono/Inspector/Editor.cs
@@ -14,6 +14,7 @@
using UnityEngine.Scripting;
using UnityEngine.UIElements;
using Unity.Collections;
+using Unity.Scripting.LifecycleManagement;
using Component = UnityEngine.Component;
using UnityObject = UnityEngine.Object;
@@ -410,7 +411,7 @@ propertyViewer is EditorWindow editorWindow
? editorWindow.dataModeController.dataMode
: DataMode.Disabled;
- internal static float kLineHeight = EditorGUI.kSingleLineHeight;
+ internal static readonly float kLineHeight = EditorGUI.kSingleLineHeight;
[VisibleToOtherModules("UnityEditor.ShaderFoundryModule")]
internal bool hideInspector = false;
@@ -420,8 +421,10 @@ propertyViewer is EditorWindow editorWindow
internal const float k_HeaderHeight = 21f;
internal delegate void OnEditorGUIDelegate(Editor editor, Rect drawRect);
+ [AutoStaticsCleanupOnCodeReload] // delegate field holding editor icon callbacks
internal static OnEditorGUIDelegate OnPostIconGUI = null;
+ [NoAutoStaticsCleanup] // enabled/disabled within a frame (Dispose pattern), safe to persist
internal static bool m_AllowMultiObjectAccess = true;
bool m_HasUnsavedChanges = false;
@@ -937,6 +940,7 @@ public virtual bool RequiresConstantRepaint()
return false;
}
+ [AutoStaticsCleanupOnCodeReload] // event holds user-registered header GUI callbacks
public static event Action finishedDefaultHeaderGUI = null;
// This is the method that should be called from externally e.g. myEditor.DrawHeader ();
diff --git a/Editor/Mono/Inspector/EditorElementUpdater.cs b/Editor/Mono/Inspector/EditorElementUpdater.cs
index 59ce1d9dc6..f6684234fc 100644
--- a/Editor/Mono/Inspector/EditorElementUpdater.cs
+++ b/Editor/Mono/Inspector/EditorElementUpdater.cs
@@ -88,31 +88,52 @@ public void CreateInspectorElementsWithoutLayout(int count)
/// Invokes followed by a layout until the given is filled.
///
/// The viewport to build elements for.
- ///
+ /// The content container to build elements into.
public void CreateInspectorElementsForViewport(ScrollView viewport, VisualElement contentContainer)
{
if (m_Index >= m_EditorElements.Count)
return;
+ // Filling the viewport is the same operation as keeping the current scroll offset reachable: build until
+ // the content extends a viewport height past it. Building can disturb the scroller, so restore it after.
var scroll = viewport.verticalScroller.value;
+ CreateInspectorElementsToReachScrollOffset(viewport, contentContainer, scroll);
+ viewport.verticalScroller.value = scroll;
+ }
+ ///
+ /// Invokes followed by a layout until the built content extends a
+ /// viewport height past , i.e. until can be applied to
+ /// the vertical scroller without being clamped.
+ ///
+ ///
+ /// Only the editors needed to make the offset reachable are built up front; the rest are left to the time-sliced
+ /// update loop. This avoids the main-thread stall of synchronously building every editor while still letting the
+ /// scroll offset be restored without clamping to 0. Used both to fill the viewport on the first build (offset =
+ /// current scroll) and to restore a saved scroll offset on a rebuild.
+ ///
+ /// The viewport being filled.
+ /// The content container to build elements into.
+ /// The vertical scroll offset that must remain reachable.
+ public void CreateInspectorElementsToReachScrollOffset(ScrollView viewport, VisualElement contentContainer, float targetOffset)
+ {
while (m_Index < m_EditorElements.Count)
{
var element = m_EditorElements[m_Index++];
element.CreateInspectorElement();
- // If this element contributes to the layout, re-compute it immediately to determine how much of the viewport is occupied.
- if (null != element.editor && InternalEditorUtility.GetIsInspectorExpanded(element.editor.target)&& contentContainer.childCount>0)
+ // If this element contributes to the layout, re-compute it immediately to measure how tall the content is.
+ if (element.editor != null && InternalEditorUtility.GetIsInspectorExpanded(element.editor.target) && contentContainer.childCount > 0)
{
Panel?.UpdateWithoutRepaint();
- if (contentContainer.ElementAt(contentContainer.childCount - 1).layout.yMax - scroll > viewport.layout.height)
+ // Stop once the built content extends past the target offset plus the viewport: the scroller's high
+ // value then covers the offset, so re-applying it won't clamp. Remaining editors only add height.
+ if (contentContainer.ElementAt(contentContainer.childCount - 1).layout.yMax - targetOffset > viewport.layout.height)
break;
}
}
-
- viewport.verticalScroller.value = scroll;
}
///
@@ -140,7 +161,7 @@ public void CreateInspectorElementsForMilliseconds(long targetMilliseconds)
break;
// If this element contributes to the layout, re-compute it immediately to determine how much of the viewport is occupied.
- if (null != element.editor && InternalEditorUtility.GetIsInspectorExpanded(element.editor.target))
+ if (element.editor != null && InternalEditorUtility.GetIsInspectorExpanded(element.editor.target))
Panel?.UpdateWithoutRepaint();
}
}
diff --git a/Editor/Mono/Inspector/EditorSettingsInspector.cs b/Editor/Mono/Inspector/EditorSettingsInspector.cs
index 90062ca820..9796a06520 100644
--- a/Editor/Mono/Inspector/EditorSettingsInspector.cs
+++ b/Editor/Mono/Inspector/EditorSettingsInspector.cs
@@ -126,6 +126,9 @@ class Content
};
public static readonly GUIContent numberingHierarchyScheme = EditorGUIUtility.TrTextContent("Game Object Naming");
public static readonly GUIContent numberingHierarchyDigits = EditorGUIUtility.TrTextContent("Game Object Digits");
+
+ public static readonly GUIContent hierarchy = EditorGUIUtility.TrTextContent("Hierarchy");
+ public static readonly GUIContent useLegacyHierarchy = EditorGUIUtility.TrTextContent("Use Legacy Hierarchy", "Use the legacy Hierarchy window.");
public static readonly GUIContent numberingProjectSpace = EditorGUIUtility.TrTextContent("Space Before Number in Asset Names");
public static GUIContent referencedClipsExactNaming = EditorGUIUtility.TrTextContent("Exactly Match Referenced Clip Names", "Controls how referenced clips are matched with models that are animated in Legacy mode. If turned on, the model name and the referenced clip names must exactly match. If turned off, only the start of the model name needs to match the referenced clip name. Also controls the behavior of the \"Update referenced clips\" button for models that are animated in Humanoid mode. See the documentation for EditorSettings.referencedClipsExactNaming for more details.");
@@ -563,6 +566,7 @@ public override void OnInspectorGUI()
DoShadersSettings();
DoEnterPlayModeSettings();
DoNumberingSchemeSettings();
+ DoHierarchySettings();
DoEnterInspectorSettings();
DoBuildProfileSettings();
@@ -957,6 +961,18 @@ private void DoStreamingSettings()
}
EditorSettings.NamingScheme m_PrevGoNamingScheme;
+ private void DoHierarchySettings()
+ {
+ GUILayout.Space(10);
+ GUILayout.Label(Content.hierarchy, EditorStyles.boldLabel);
+
+ EditorGUI.BeginChangeCheck();
+ bool useLegacyHierarchy = EditorSettings.useLegacyHierarchy;
+ useLegacyHierarchy = EditorGUILayout.Toggle(Content.useLegacyHierarchy, useLegacyHierarchy);
+ if (EditorGUI.EndChangeCheck())
+ EditorSettings.useLegacyHierarchy = useLegacyHierarchy;
+ }
+
int m_PrevGoNamingDigits = -1;
string m_GoNamingHelpText;
static string GetNewName(string name, List names)
@@ -1173,6 +1189,7 @@ private void SetSpritePackerMode(object data)
// Legacy Packer has been obsoleted (1 & 2). Disabled (0) is still valid.
popupIndex = (popupIndex != 0) ? (popupIndex + spritePackDeprecatedEnums) : 0;
+ bool refreshSprite = (m_SpritePackerMode.intValue != popupIndex);
m_SpritePackerMode.intValue = popupIndex;
if (m_IsGlobalSettings)
@@ -1183,6 +1200,11 @@ private void SetSpritePackerMode(object data)
UnityEditor.U2D.SpriteAtlasImporter.MigrateAllSpriteAtlases();
}
}
+
+ if (refreshSprite)
+ {
+ UnityEditor.U2D.SpriteAtlasUtility.OnSpriteAtlasSettingsChanged();
+ }
}
private void SetRefreshImportMode(object data)
diff --git a/Editor/Mono/Inspector/GraphicsSettingsInspector.cs b/Editor/Mono/Inspector/GraphicsSettingsInspector.cs
index bebf999fae..7d06a6c636 100644
--- a/Editor/Mono/Inspector/GraphicsSettingsInspector.cs
+++ b/Editor/Mono/Inspector/GraphicsSettingsInspector.cs
@@ -130,8 +130,8 @@ void Setup(bool globalSettingsExist)
SetupBiRPDeprecationInfoBox(m_CurrentRoot);
- BindEnumFieldWithFadeGroup(m_CurrentRoot, "Lightmap", ShaderUtil.CalculateLightmapStrippingFromCurrentScene);
- BindEnumFieldWithFadeGroup(m_CurrentRoot, "Fog", ShaderUtil.CalculateFogStrippingFromCurrentScene);
+ BindEnumFieldWithFadeGroup(m_CurrentRoot, "Lightmap", CalculateLightmapStrippingFromCurrentScene);
+ BindEnumFieldWithFadeGroup(m_CurrentRoot, "Fog", CalculateFogStrippingFromCurrentScene);
BindEnumFieldToLightProbe(m_CurrentRoot);
BindEnumFieldToDefaultLightBaker(m_CurrentRoot);
@@ -173,10 +173,23 @@ void Setup(bool globalSettingsExist)
m_CurrentRoot.Bind(serializedObject);
}
+ void CalculateLightmapStrippingFromCurrentScene()
+ {
+ Undo.RegisterCompleteObjectUndo(target, L10n.Tr("Calculate Lightmap Stripping From Current Scene"));
+ ShaderUtil.CalculateLightmapStrippingFromCurrentScene();
+ }
+
+ void CalculateFogStrippingFromCurrentScene()
+ {
+ Undo.RegisterCompleteObjectUndo(target, L10n.Tr("Calculate Fog Stripping From Current Scene"));
+ ShaderUtil.CalculateFogStrippingFromCurrentScene();
+ }
+
void BindShaderPreload(VisualElement root)
{
var shaderPreloadProperty = serializedObject.FindProperty("m_PreloadedShaders");
shaderPreloadProperty.isExpanded = false;
+ var recommendGSCInfoBox = root.MandatoryQ("RecommendGSCInfoBox");
var shaderPreloadPropertyField = root.MandatoryQ("PreloadedShaders");
shaderPreloadPropertyField.onGUIHandler = () =>
@@ -188,7 +201,7 @@ void BindShaderPreload(VisualElement root)
if (EditorGUI.EndChangeCheck())
{
shaderPreloadProperty.serializedObject.ApplyModifiedProperties();
- UIElementsEditorUtility.SetVisibility(root.MandatoryQ("RecommendGSCInfoBox"), shaderPreloadProperty.arraySize > 0 && shaderPreloadProperty.GetArrayElementAtIndex(0).objectReferenceValue);
+ UIElementsEditorUtility.SetVisibility(recommendGSCInfoBox, shaderPreloadProperty.arraySize > 0 && shaderPreloadProperty.GetArrayElementAtIndex(0).objectReferenceValue);
}
};
@@ -196,7 +209,18 @@ void BindShaderPreload(VisualElement root)
var shaderPreloadToggle = root.MandatoryQ("ShaderPreloadToggle");
var delayedShaderTimeLimitGroup = root.MandatoryQ("DelayedShaderTimeLimitGroup");
var delayedShaderTimeLimit = root.MandatoryQ("DelayedShaderTimeLimit");
- shaderPreloadToggle.RegisterValueChangedCallback(evt => {
+
+ void RefreshFromProperty()
+ {
+ var value = delayedShaderTimeLimitProperty.intValue;
+ var enabled = value >= 0;
+ shaderPreloadToggle.SetValueWithoutNotify(enabled);
+ delayedShaderTimeLimit.SetValueWithoutNotify(Mathf.Max(0, value));
+ delayedShaderTimeLimitGroup.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+
+ shaderPreloadToggle.RegisterValueChangedCallback(evt =>
+ {
delayedShaderTimeLimitGroup.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None;
var newVal = evt.newValue ? delayedShaderTimeLimit.value : -1;
if (delayedShaderTimeLimitProperty.intValue != newVal)
@@ -213,9 +237,10 @@ void BindShaderPreload(VisualElement root)
delayedShaderTimeLimitProperty.serializedObject.ApplyModifiedProperties();
}
});
- shaderPreloadToggle.SetValueWithoutNotify(delayedShaderTimeLimitProperty.intValue >= 0);
- delayedShaderTimeLimit.SetValueWithoutNotify(Mathf.Max(0, delayedShaderTimeLimitProperty.intValue));
- delayedShaderTimeLimitGroup.style.display = delayedShaderTimeLimitProperty.intValue >= 0 ? DisplayStyle.Flex : DisplayStyle.None;
+
+ RefreshFromProperty();
+ // Re-sync when the property changes externally (e.g. Reset / Undo).
+ shaderPreloadToggle.TrackPropertyValue(delayedShaderTimeLimitProperty, _ => RefreshFromProperty());
var shaderTracking = root.MandatoryQ("ShaderTrackingInfoBox");
shaderTracking.schedule.Execute(() =>
@@ -377,7 +402,6 @@ void BindEnumFieldWithFadeGroup(VisualElement content, string id, Action buttonC
var enumMode = content.MandatoryQ($"{id}Modes");
var enumModeGroup = content.MandatoryQ($"{id}ModesGroup");
var enumModeProperty = serializedObject.FindProperty($"m_{id}Stripping");
- UIElementsEditorUtility.SetVisibility(enumModeGroup, (StrippingModes)enumModeProperty.enumValueFlag == StrippingModes.Custom);
UIElementsEditorUtility.BindSerializedProperty(enumMode, enumModeProperty,
mode => UIElementsEditorUtility.SetVisibility(enumModeGroup, mode == StrippingModes.Custom));
content.MandatoryQ($"Import{id}FromCurrentScene").clicked += buttonCallback;
diff --git a/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorTierSettings.cs b/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorTierSettings.cs
index e11e7881f2..1ae2536a4e 100644
--- a/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorTierSettings.cs
+++ b/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorTierSettings.cs
@@ -170,10 +170,20 @@ void Draw()
{
using var settingsScope = new LabelWidthScope();
using var wideScreenScope = new WideScreenScope(this);
- if (m_TierSettingsAnimator == null)
- OnInspectorGUI();
- else
- TierSettingsGUI();
+
+ var previousTextClipping = EditorStyles.label.clipping;
+ EditorStyles.label.clipping = TextClipping.Ellipsis;
+ try
+ {
+ if (m_TierSettingsAnimator == null)
+ OnInspectorGUI();
+ else
+ TierSettingsGUI();
+ }
+ finally
+ {
+ EditorStyles.label.clipping = previousTextClipping;
+ }
}
void HandleEditorWindowButton()
diff --git a/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorUtility.cs b/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorUtility.cs
index 2d263e2a9d..639ade64cf 100644
--- a/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorUtility.cs
+++ b/Editor/Mono/Inspector/GraphicsSettingsInspectors/GraphicsSettingsInspectorUtility.cs
@@ -353,6 +353,19 @@ public static void OpenAndScrollTo(string propertyPath)
(scrollView, _, propertyField, _) => OpenFoldoutsThenScroll(propertyField, scrollView));
}
+ public static void OpenAndScrollToElement(string elementName)
+ {
+ if (string.IsNullOrEmpty(elementName))
+ throw new ArgumentException(nameof(elementName), $"The {nameof(elementName)} argument can't be null or empty.");
+
+ OpenAndScrollTo(
+ root => TryFindElementAndTabByName(elementName, root, out var tabbedView, out var tabButton, out var element)
+ ? (true, tabbedView, tabButton, element)
+ : (false, null, null, null),
+ () => $"Couldn't find an element with name {elementName} in the settings container.",
+ (scrollView, _, element, _) => OpenFoldoutsThenScroll(element, scrollView));
+ }
+
public static void OpenAndScrollTo(Type renderPipelineGraphicsSettingsType)
{
OpenAndScrollTo(renderPipelineGraphicsSettingsType);
@@ -485,6 +498,20 @@ static bool TryFindPropertyAndTabByBindingPath(string propertyPath, VisualElemen
return true;
}
+ static bool TryFindElementAndTabByName(string elementName, VisualElement root, out TabbedView tabbedView, out TabButton tabButton, out VisualElement element)
+ {
+ tabbedView = root.Q();
+ tabButton = null;
+
+ element = root.Q(elementName);
+ if (element == null)
+ return false;
+
+ if (tabbedView != null)
+ tabButton = element.GetFirstAncestorOfType();
+ return true;
+ }
+
///
/// Find the tab and binding path for the Render Pipeline Graphics Settings type.
///
diff --git a/Editor/Mono/Inspector/LightingSettingsEditor.cs b/Editor/Mono/Inspector/LightingSettingsEditor.cs
index d21362b6f7..c626f8ee4e 100644
--- a/Editor/Mono/Inspector/LightingSettingsEditor.cs
+++ b/Editor/Mono/Inspector/LightingSettingsEditor.cs
@@ -3,6 +3,7 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using UnityEditor.Inspector.GraphicsSettingsInspectors;
using UnityEngine.Rendering;
using UnityEngine;
using UnityEngineInternal;
@@ -133,18 +134,20 @@ static class Styles
{
public static readonly float buttonWidth = 200;
+#pragma warning disable 618 // ProgressiveCPU is deprecated but still selectable in the UI.
static readonly int[] k_BakeBackendValues =
{
(int)LightingSettings.Lightmapper.ProgressiveCPU,
(int)LightingSettings.Lightmapper.ProgressiveGPU
};
+#pragma warning restore 618
static readonly int[] k_BakeBackendValuesWithUnityComputeGPU =
k_BakeBackendValues.ConcatValue((int)LightingSettings.Lightmapper.UnityComputeGPU);
public static int[] bakeBackendValues => (UnityEditor.Rendering.EditorGraphicsSettings.defaultLightBaker == UnityEditor.Rendering.LightBaker.UnityComputeLightBaker) ? k_BakeBackendValuesWithUnityComputeGPU : k_BakeBackendValues;
public static readonly GUIContent[] k_BakeBackendStrings =
{
- EditorGUIUtility.TrTextContent("Progressive CPU"),
+ EditorGUIUtility.TrTextContent("Progressive CPU (Deprecated)"),
EditorGUIUtility.TrTextContent("Progressive GPU")
};
static readonly GUIContent[] k_BakeBackendStringsWithUnityComputeGPU =
@@ -254,6 +257,14 @@ static class Styles
};
public static readonly GUIContent lightmapperNotSupportedWarning = EditorGUIUtility.TrTextContent("This lightmapper is not supported by the current Render Pipeline. The Editor will use ");
+ public static readonly GUIContent progressiveCpuDeprecationWarning = EditorGUIUtility.TrTextContent("Progressive CPU will be removed in a future release. Please use the Unity Compute Light Baker instead.");
+ public static readonly GUIContent openGraphicsSettings = EditorGUIUtility.TrTextContent("Open", "Open the Graphics Settings and select the Default Light Baker.");
+ static GUIStyle s_DeprecationHelpBoxLabel;
+ // wordWrappedLabel with a small vertical offset so the first line of text visually aligns with the icon.
+ public static GUIStyle deprecationHelpBoxLabel => s_DeprecationHelpBoxLabel ??= new GUIStyle(EditorStyles.wordWrappedLabel)
+ {
+ contentOffset = new Vector2(0, -3)
+ };
public static readonly GUIContent mixedModeNotSupportedWarning = EditorGUIUtility.TrTextContent("The Mixed mode is not supported by the current Render Pipeline. Fallback mode is ");
public static readonly GUIContent directionalNotSupportedWarning = EditorGUIUtility.TrTextContent("Directional Mode is not supported. Fallback will be Non-Directional.");
public static readonly GUIContent denoiserNotSupportedWarning = EditorGUIUtility.TrTextContent("The current hardware or system configuration does not support the selected denoiser. Select a different denoiser.");
@@ -613,10 +624,10 @@ void GeneralLightmapSettingsGUI(bool compact)
EditorGUI.indentLevel++;
if (!usingComputeLightBaker)
EditorGUILayout.PropertyField(m_PVREnvironmentIS, Styles.environmentImportanceSampling);
- MultiEditableLogarithmicIntSlider(m_PVRDirectSampleCount, Styles.directSampleCount, 1, maxDirectSamples, 1, 1 << 30);
- MultiEditableLogarithmicIntSlider(m_PVRSampleCount, Styles.indirectSampleCount, 1, maxIndirectSamples, 1, 1 << 30);
+ MultiEditableLogarithmicIntSlider(m_PVRDirectSampleCount, Styles.directSampleCount, 1, maxDirectSamples, 1, maxDirectSamples);
+ MultiEditableLogarithmicIntSlider(m_PVRSampleCount, Styles.indirectSampleCount, 1, maxIndirectSamples, 1, maxIndirectSamples);
if (!usingComputeLightBaker)
- MultiEditableLogarithmicIntSlider(m_PVREnvironmentSampleCount, Styles.environmentSampleCount, 1, maxEnvironmentSamples, 1, 1 << 30);
+ MultiEditableLogarithmicIntSlider(m_PVREnvironmentSampleCount, Styles.environmentSampleCount, 1, maxEnvironmentSamples, 1, maxEnvironmentSamples);
maxDirectSamples = (int)Mathf.ClosestPowerOfTwo(Math.Max(maxDirectSamples, m_PVRDirectSampleCount.intValue));
maxIndirectSamples = (int)Mathf.ClosestPowerOfTwo(Math.Max(maxIndirectSamples, m_PVRSampleCount.intValue));
@@ -1125,6 +1136,40 @@ void BakeBackendGUI()
string fallbackLightmapper = Styles.bakeBackendStrings[SupportedRenderingFeatures.FallbackLightmapper()].text;
EditorGUILayout.HelpBox(Styles.lightmapperNotSupportedWarning.text + fallbackLightmapper + " Lightmapper instead.", MessageType.Warning);
}
+
+#pragma warning disable 618 // ProgressiveCPU is deprecated; comparison drives the deprecation HelpBox.
+ if (m_BakeBackend.intValue == (int)LightingSettings.Lightmapper.ProgressiveCPU)
+ DrawProgressiveCpuDeprecationHelpBox();
+#pragma warning restore 618
+ }
+
+ static void DrawProgressiveCpuDeprecationHelpBox()
+ {
+ const float kButtonHeight = 20f;
+ const float kButtonWidth = 60f;
+ const float kIconSize = 16f;
+
+ using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
+ {
+ int oldIndent = EditorGUI.indentLevel;
+ EditorGUI.indentLevel = 0;
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon.sml"), GUIStyle.none,
+ GUILayout.Width(kIconSize), GUILayout.Height(kIconSize));
+ EditorGUILayout.LabelField(Styles.progressiveCpuDeprecationWarning.text, Styles.deprecationHelpBoxLabel);
+ }
+
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button(Styles.openGraphicsSettings, GUILayout.Width(kButtonWidth), GUILayout.Height(kButtonHeight)))
+ GraphicsSettingsInspectorUtility.OpenAndScrollToElement("DefaultLightBaker");
+ }
+ EditorGUI.indentLevel = oldIndent;
+ GUILayout.Space(3);
+ }
}
void OnDirectDenoiserSelected(object userData)
diff --git a/Editor/Mono/Inspector/LineRendererCurveEditor.cs b/Editor/Mono/Inspector/LineRendererCurveEditor.cs
index 1ffdbd126f..5d94be8500 100644
--- a/Editor/Mono/Inspector/LineRendererCurveEditor.cs
+++ b/Editor/Mono/Inspector/LineRendererCurveEditor.cs
@@ -10,7 +10,7 @@ internal class LineRendererCurveEditor
{
private class Styles
{
- public static GUIContent widthMultiplier = EditorGUIUtility.TrTextContent("Width", "The multiplier applied to the curve, describing the width (in world space) along the line.");
+ public static readonly GUIContent widthMultiplier = EditorGUIUtility.TrTextContent("Width", "The multiplier applied to the curve, describing the width (in world space) along the line.");
}
private bool m_Refresh = false;
diff --git a/Editor/Mono/Inspector/MemorySettingsEditor.cs b/Editor/Mono/Inspector/MemorySettingsEditor.cs
index aec24401bf..89718a9189 100644
--- a/Editor/Mono/Inspector/MemorySettingsEditor.cs
+++ b/Editor/Mono/Inspector/MemorySettingsEditor.cs
@@ -2,12 +2,14 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using UnityEditorInternal;
+using UnityEngine.Analytics;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
using UnityEditor.Callbacks;
@@ -40,6 +42,7 @@ class Content
public static readonly GUIContent kExtraAllocatorTitle = EditorGUIUtility.TrTextContent("Other Allocators");
public static readonly GUIContent kCacheBlockSize = EditorGUIUtility.TrTextContent("File Cache Block Size", "Block size used by file cache allocator. Setting this value to 0 will cause the file cache allocations to be passed to the main allocator");
public static readonly GUIContent kTypetreeBlockSize = EditorGUIUtility.TrTextContent("Type Tree Block Size", "Block size used by the tree allocator. Setting this value to 0 will cause the type tree allocations to be passed to the main allocator");
+ public static readonly GUIContent kRemapperInitialCapacity = EditorGUIUtility.TrTextContent("Remapper Initial Capacity", "Initial capacity of the Remapper allocation");
public static readonly GUIContent kTempAllocatorTitle_Player = EditorGUIUtility.TrTextContent("Fast Per Thread Temporary Allocators", "Block size can grow to twice the initial size");
public static readonly GUIContent kTempAllocatorTitle_Editor = EditorGUIUtility.TrTextContent("Fast Per Thread Temporary Allocators", "Block size can grow to 8 times the initial size");
@@ -92,6 +95,7 @@ class Styles
}
const string kWarningDialogSessionKey = "MemorySettingsWarning";
+ const string kMainAllocatorMimallocEnabledPropertyName = "m_MainAllocatorMimallocEnabled";
SerializedProperty m_PlatformMemorySettingsProperty;
SerializedProperty m_EditorMemorySettingsProperty;
@@ -201,6 +205,31 @@ private void EndGroup()
EditorGUILayout.EndVertical();
}
+ bool GetEffectiveMimallocEnabled(SerializedProperty settings)
+ {
+ var mimallocProperty = settings.FindPropertyRelative(kMainAllocatorMimallocEnabledPropertyName);
+ var defaultValueProperty = m_DefaultMemorySettingsProperty.FindPropertyRelative(kMainAllocatorMimallocEnabledPropertyName);
+ var resolvedValue = mimallocProperty.intValue < 0 ? defaultValueProperty.intValue : mimallocProperty.intValue;
+
+ return resolvedValue != 0;
+ }
+
+ static bool UsesDefaultMimallocSetting(SerializedProperty settings)
+ {
+ return settings.FindPropertyRelative(kMainAllocatorMimallocEnabledPropertyName).intValue < 0;
+ }
+
+ void ApplyChangesAndSendMimallocAnalytics(SerializedProperty currentSettings, bool previousMimallocEnabled, string buildTarget)
+ {
+ serializedObject.ApplyModifiedProperties();
+
+ var mimallocEnabled = GetEffectiveMimallocEnabled(currentSettings);
+ if (previousMimallocEnabled == mimallocEnabled)
+ return;
+
+ MemorySettingsAnalytics.SendMimallocSettingChanged(mimallocEnabled, UsesDefaultMimallocSetting(currentSettings), m_EditorSelected ? "editor" : "player", buildTarget);
+ }
+
enum SizeEnum
{
B,
@@ -452,6 +481,15 @@ public override void OnInspectorGUI()
MemorySettingsUtils.InitializeDefaultsForPlatform((int)m_ValidPlatforms[m_SelectedPlatform].defaultTarget);
}
+ var previousMimallocEnabled = GetEffectiveMimallocEnabled(currentSettings);
+ var buildTarget = m_EditorSelected ? "Editor" : m_ValidPlatforms[m_SelectedPlatform].defaultTarget.ToString();
+ // iOS/tvOS/visionOS never use mimalloc (the engine always selects the system allocator there),
+ // so the setting is shown disabled/off and emits no analytics.
+ bool isAppleNonDesktop = !m_EditorSelected &&
+ (m_ValidPlatforms[m_SelectedPlatform].defaultTarget == BuildTarget.iOS
+ || m_ValidPlatforms[m_SelectedPlatform].defaultTarget == BuildTarget.tvOS
+ || m_ValidPlatforms[m_SelectedPlatform].defaultTarget == BuildTarget.VisionOS);
+
if (BeginGroup(0, Content.kMainAllocatorsTitle))
{
if (BeginGroup(1, Content.kMainAllocatorTitle))
@@ -465,7 +503,20 @@ public override void OnInspectorGUI()
OptionalVariableField(currentSettings, "m_ThreadAllocatorBlockSize", Content.kThreadAllocatorBlockSize);
}
- OptionalBooleanField(currentSettings, "m_MainAllocatorMimallocEnabled", Content.kMainAllocatorMimallocEnabled);
+ if (isAppleNonDesktop)
+ {
+ // Not supported on iOS/tvOS/visionOS: show disabled + off, and clear any stored override.
+ var mimallocProp = currentSettings.FindPropertyRelative(kMainAllocatorMimallocEnabledPropertyName);
+ if (mimallocProp.intValue != -1)
+ mimallocProp.intValue = -1;
+ using (new EditorGUI.DisabledScope(true))
+ EditorGUILayout.Toggle(Content.kMainAllocatorMimallocEnabled, false);
+ EditorGUILayout.HelpBox("Mimalloc is not supported on iOS, tvOS and visionOS. These platforms always use the system allocator.", MessageType.Info);
+ }
+ else
+ {
+ OptionalBooleanField(currentSettings, kMainAllocatorMimallocEnabledPropertyName, Content.kMainAllocatorMimallocEnabled);
+ }
}
EndGroup();
if (BeginGroup(2, Content.kGfxAllocatorTitle))
@@ -478,6 +529,7 @@ public override void OnInspectorGUI()
{
OptionalVariableField(currentSettings, "m_CacheBlockSize", Content.kCacheBlockSize);
OptionalVariableField(currentSettings, "m_TypetreeBlockSize", Content.kTypetreeBlockSize);
+ OptionalVariableField(currentSettings, "m_RemapperInitialCapacity", Content.kRemapperInitialCapacity);
}
EndGroup();
if (BeginGroup(4, Content.kBucketAllocatorTitle))
@@ -533,14 +585,17 @@ public override void OnInspectorGUI()
{
if (EditorGUI.EndChangeCheck())
{
- serializedObject.ApplyModifiedProperties();
+ ApplyChangesAndSendMimallocAnalytics(currentSettings, previousMimallocEnabled, buildTarget);
MemorySettingsUtils.WriteEditorMemorySettings();
}
}
else
{
EditorGUILayout.EndPlatformGrouping();
- serializedObject.ApplyModifiedProperties();
+ if (isAppleNonDesktop)
+ serializedObject.ApplyModifiedProperties(); // persist the cleared override, but emit no analytics (mimalloc isn't a user choice here)
+ else
+ ApplyChangesAndSendMimallocAnalytics(currentSettings, previousMimallocEnabled, buildTarget);
}
EditorGUILayout.EndVertical();
@@ -556,4 +611,93 @@ internal static SettingsProvider CreateProjectSettingsProvider()
return provider;
}
}
+
+ internal interface IMemorySettingsAnalyticsService
+ {
+ AnalyticsResult SendAnalytic(IAnalytic analytic);
+ }
+
+ internal class MemorySettingsEditorAnalyticsService : IMemorySettingsAnalyticsService
+ {
+ AnalyticsResult IMemorySettingsAnalyticsService.SendAnalytic(IAnalytic analytic)
+ {
+ return EditorAnalytics.SendAnalytic(analytic);
+ }
+ }
+
+ internal static class MemorySettingsAnalytics
+ {
+ const string k_BuildTargetEditor = "Editor";
+ const string k_EventName = "mimallocSettingChanged";
+ const int k_MaxEventsPerHour = 100;
+ const string k_VendorKey = "unity.memory";
+ static Action s_TestEventCallback;
+
+ [Serializable]
+ internal struct MimallocSettingChangedData : IAnalytic.IData
+ {
+ public bool enabled;
+ public bool uses_default;
+ public string scope;
+ public string build_target;
+ }
+
+ [AnalyticInfo(eventName: k_EventName, vendorKey: k_VendorKey, version: 1, maxEventsPerHour: k_MaxEventsPerHour)]
+ internal class MimallocSettingChangedAnalytic : IAnalytic
+ {
+ readonly MimallocSettingChangedData m_Data;
+
+ public MimallocSettingChangedAnalytic(MimallocSettingChangedData data)
+ {
+ m_Data = data;
+ }
+
+ public bool TryGatherData(out IAnalytic.IData data, out Exception error)
+ {
+ data = m_Data;
+ error = null;
+ return true;
+ }
+ }
+
+ static IMemorySettingsAnalyticsService s_AnalyticsService;
+
+ static MemorySettingsAnalytics()
+ {
+ if (!InternalEditorUtility.inBatchMode && EditorAnalytics.enabled)
+ SetAnalyticsService(new MemorySettingsEditorAnalyticsService());
+ }
+
+ public static IMemorySettingsAnalyticsService SetAnalyticsService(IMemorySettingsAnalyticsService service)
+ {
+ var oldService = s_AnalyticsService;
+ s_AnalyticsService = service;
+ return oldService;
+ }
+
+ internal static Action SetTestEventCallback(Action callback)
+ {
+ var oldCallback = s_TestEventCallback;
+ s_TestEventCallback = callback;
+ return oldCallback;
+ }
+
+ public static void SendMimallocSettingChanged(bool enabled, bool usesDefault, string scope, string buildTarget)
+ {
+ var resolvedBuildTarget = string.IsNullOrEmpty(buildTarget) ? k_BuildTargetEditor : buildTarget;
+
+ s_TestEventCallback?.Invoke(enabled, usesDefault, scope, resolvedBuildTarget);
+
+ if (s_AnalyticsService == null)
+ return;
+
+ s_AnalyticsService.SendAnalytic(new MimallocSettingChangedAnalytic(new MimallocSettingChangedData
+ {
+ enabled = enabled,
+ uses_default = usesDefault,
+ scope = scope,
+ build_target = resolvedBuildTarget,
+ }));
+ }
+ }
}
diff --git a/Editor/Mono/Inspector/ParticleSystemForceFieldInspector.cs b/Editor/Mono/Inspector/ParticleSystemForceFieldInspector.cs
index 6b1ee1335a..d81ac75f1e 100644
--- a/Editor/Mono/Inspector/ParticleSystemForceFieldInspector.cs
+++ b/Editor/Mono/Inspector/ParticleSystemForceFieldInspector.cs
@@ -7,6 +7,7 @@
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
@@ -24,16 +25,22 @@ protected override void DrawWireframe()
[CanEditMultipleObjects]
internal class ParticleSystemForceFieldInspector : Editor
{
+ [NoAutoStaticsCleanup] // bounds handle created once; stateless between uses
private static readonly SphereBoundsHandle s_SphereBoundsHandle = new SphereBoundsHandle();
+ [NoAutoStaticsCleanup] // bounds handle created once; stateless between uses
private static readonly UniformBoxBoundsHandle s_BoxBoundsHandle = new UniformBoxBoundsHandle();
- private static PrefColor s_GizmoColor = new PrefColor("Particle System/Force Field Gizmos", 148f / 255f, 229f / 255f, 1f, 0.9f);
+ private static readonly PrefColor s_GizmoColor = new PrefColor("Particle System/Force Field Gizmos", 148f / 255f, 229f / 255f, 1f, 0.9f);
private static readonly Color s_GizmoFocusTint = new Color(0.7f, 0.7f, 0.7f, 1.0f);
- private static PropertyInfo s_StartRangeProperty = typeof(ParticleSystemForceField).GetProperty("startRange");
- private static PropertyInfo s_EndRangeProperty = typeof(ParticleSystemForceField).GetProperty("endRange");
- private static PropertyInfo s_GravityFocusProperty = typeof(ParticleSystemForceField).GetProperty("gravityFocus");
- private static PropertyInfo s_LengthProperty = typeof(ParticleSystemForceField).GetProperty("length");
+ [NoAutoStaticsCleanup] // reflection cache for immutable type metadata
+ private static readonly PropertyInfo s_StartRangeProperty = typeof(ParticleSystemForceField).GetProperty("startRange");
+ [NoAutoStaticsCleanup] // reflection cache for immutable type metadata
+ private static readonly PropertyInfo s_EndRangeProperty = typeof(ParticleSystemForceField).GetProperty("endRange");
+ [NoAutoStaticsCleanup] // reflection cache for immutable type metadata
+ private static readonly PropertyInfo s_GravityFocusProperty = typeof(ParticleSystemForceField).GetProperty("gravityFocus");
+ [NoAutoStaticsCleanup] // reflection cache for immutable type metadata
+ private static readonly PropertyInfo s_LengthProperty = typeof(ParticleSystemForceField).GetProperty("length");
private static readonly string s_UndoString = L10n.Tr("Modify {0}");
@@ -68,26 +75,26 @@ internal class ParticleSystemForceFieldInspector : Editor
private class Styles
{
- public static GUIContent shape = EditorGUIUtility.TrTextContent("Shape", "The bounding shape that forces are applied inside.");
- public static GUIContent startRange = EditorGUIUtility.TrTextContent("Start Range", "The inner extent of the bounding shape.");
- public static GUIContent endRange = EditorGUIUtility.TrTextContent("End Range", "The outer extent of the bounding shape.");
- public static GUIContent length = EditorGUIUtility.TrTextContent("Length", "The length of the cylinder.");
- public static GUIContent directionX = EditorGUIUtility.TrTextContent("X", "The force to apply along the X axis.");
- public static GUIContent directionY = EditorGUIUtility.TrTextContent("Y", "The force to apply along the Y axis.");
- public static GUIContent directionZ = EditorGUIUtility.TrTextContent("Z", "The force to apply along the Z axis.");
- public static GUIContent gravity = EditorGUIUtility.TrTextContent("Strength", "The strength of the gravity effect.");
- public static GUIContent gravityFocus = EditorGUIUtility.TrTextContent("Focus", "Choose a band within the volume that particles will be attracted towards.");
- public static GUIContent rotationSpeed = EditorGUIUtility.TrTextContent("Speed", "The speed at which particles are propelled around the vortex.");
- public static GUIContent rotationAttraction = EditorGUIUtility.TrTextContent("Attraction", "Controls how strongly particles are dragged into the vortex motion.");
- public static GUIContent rotationRandomness = EditorGUIUtility.TrTextContent("Randomness", "Propel particles around random axes of the shape.");
- public static GUIContent drag = EditorGUIUtility.TrTextContent("Strength", "The strength of the drag effect.");
- public static GUIContent multiplyDragByParticleSize = EditorGUIUtility.TrTextContent("Multiply by Size", "Adjust the drag based on the size of the particles.");
- public static GUIContent multiplyDragByParticleVelocity = EditorGUIUtility.TrTextContent("Multiply by Velocity", "Adjust the drag based on the velocity of the particles.");
- public static GUIContent vectorField = EditorGUIUtility.TrTextContent("Volume Texture", "The texture used for the vector field.");
- public static GUIContent vectorFieldSpeed = EditorGUIUtility.TrTextContent("Speed", "The speed multiplier applied to particles traveling through the vector field.");
- public static GUIContent vectorFieldAttraction = EditorGUIUtility.TrTextContent("Attraction", "Controls how strongly particles are dragged into the vector field motion.");
-
- public static GUIContent[] shapeOptions =
+ public static readonly GUIContent shape = EditorGUIUtility.TrTextContent("Shape", "The bounding shape that forces are applied inside.");
+ public static readonly GUIContent startRange = EditorGUIUtility.TrTextContent("Start Range", "The inner extent of the bounding shape.");
+ public static readonly GUIContent endRange = EditorGUIUtility.TrTextContent("End Range", "The outer extent of the bounding shape.");
+ public static readonly GUIContent length = EditorGUIUtility.TrTextContent("Length", "The length of the cylinder.");
+ public static readonly GUIContent directionX = EditorGUIUtility.TrTextContent("X", "The force to apply along the X axis.");
+ public static readonly GUIContent directionY = EditorGUIUtility.TrTextContent("Y", "The force to apply along the Y axis.");
+ public static readonly GUIContent directionZ = EditorGUIUtility.TrTextContent("Z", "The force to apply along the Z axis.");
+ public static readonly GUIContent gravity = EditorGUIUtility.TrTextContent("Strength", "The strength of the gravity effect.");
+ public static readonly GUIContent gravityFocus = EditorGUIUtility.TrTextContent("Focus", "Choose a band within the volume that particles will be attracted towards.");
+ public static readonly GUIContent rotationSpeed = EditorGUIUtility.TrTextContent("Speed", "The speed at which particles are propelled around the vortex.");
+ public static readonly GUIContent rotationAttraction = EditorGUIUtility.TrTextContent("Attraction", "Controls how strongly particles are dragged into the vortex motion.");
+ public static readonly GUIContent rotationRandomness = EditorGUIUtility.TrTextContent("Randomness", "Propel particles around random axes of the shape.");
+ public static readonly GUIContent drag = EditorGUIUtility.TrTextContent("Strength", "The strength of the drag effect.");
+ public static readonly GUIContent multiplyDragByParticleSize = EditorGUIUtility.TrTextContent("Multiply by Size", "Adjust the drag based on the size of the particles.");
+ public static readonly GUIContent multiplyDragByParticleVelocity = EditorGUIUtility.TrTextContent("Multiply by Velocity", "Adjust the drag based on the velocity of the particles.");
+ public static readonly GUIContent vectorField = EditorGUIUtility.TrTextContent("Volume Texture", "The texture used for the vector field.");
+ public static readonly GUIContent vectorFieldSpeed = EditorGUIUtility.TrTextContent("Speed", "The speed multiplier applied to particles traveling through the vector field.");
+ public static readonly GUIContent vectorFieldAttraction = EditorGUIUtility.TrTextContent("Attraction", "Controls how strongly particles are dragged into the vector field motion.");
+
+ public static readonly GUIContent[] shapeOptions =
{
EditorGUIUtility.TrTextContent("Sphere"),
EditorGUIUtility.TrTextContent("Hemisphere"),
@@ -95,12 +102,12 @@ private class Styles
EditorGUIUtility.TrTextContent("Box")
};
- public static GUIContent shapeHeading = EditorGUIUtility.TrTextContent("Shape");
- public static GUIContent directionHeading = EditorGUIUtility.TrTextContent("Direction");
- public static GUIContent gravityHeading = EditorGUIUtility.TrTextContent("Gravity");
- public static GUIContent rotationHeading = EditorGUIUtility.TrTextContent("Rotation");
- public static GUIContent dragHeading = EditorGUIUtility.TrTextContent("Drag");
- public static GUIContent vectorFieldHeading = EditorGUIUtility.TrTextContent("Vector Field");
+ public static readonly GUIContent shapeHeading = EditorGUIUtility.TrTextContent("Shape");
+ public static readonly GUIContent directionHeading = EditorGUIUtility.TrTextContent("Direction");
+ public static readonly GUIContent gravityHeading = EditorGUIUtility.TrTextContent("Gravity");
+ public static readonly GUIContent rotationHeading = EditorGUIUtility.TrTextContent("Rotation");
+ public static readonly GUIContent dragHeading = EditorGUIUtility.TrTextContent("Drag");
+ public static readonly GUIContent vectorFieldHeading = EditorGUIUtility.TrTextContent("Vector Field");
}
void OnEnable()
diff --git a/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsEditor.cs b/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsEditor.cs
index dbc7ec1cf3..8445d7fcc5 100644
--- a/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsEditor.cs
+++ b/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsEditor.cs
@@ -24,6 +24,8 @@
using UnityEngine.Bindings;
using UnityEditor.Build.Profile;
using Unity.Collections;
+using System.Collections.Immutable;
+using Unity.Scripting.LifecycleManagement;
// ************************************* READ BEFORE EDITING **************************************
//
@@ -233,7 +235,6 @@ class SettingsContent
public static readonly GUIContent autoGraphicsAPIForLinux = EditorGUIUtility.TrTextContent("Auto Graphics API for Linux");
public static readonly GUIContent iOSURLSchemes = EditorGUIUtility.TrTextContent("Supported URL schemes*");
- public static readonly GUIContent require31 = EditorGUIUtility.TrTextContent("Require ES3.1");
public static readonly GUIContent requireAEP = EditorGUIUtility.TrTextContent("Require ES3.1+AEP");
public static readonly GUIContent require32 = EditorGUIUtility.TrTextContent("Require ES3.2");
public static readonly GUIContent skinOnGPU = EditorGUIUtility.TrTextContent("GPU Skinning*", "Calculate mesh skinning and blend shapes on the GPU via shaders");
@@ -336,7 +337,7 @@ PlayerSettingsIconsEditor iconsEditor
}
}
- private static MeshDeformation[] m_MeshDeformations = { MeshDeformation.CPU, MeshDeformation.GPU, MeshDeformation.GPUBatched };
+ private static readonly MeshDeformation[] m_MeshDeformations = { MeshDeformation.CPU, MeshDeformation.GPU, MeshDeformation.GPUBatched };
internal static void SyncEditors(BuildTarget target)
{
@@ -466,8 +467,7 @@ internal static void SyncEditors(BuildTarget target)
SerializedProperty m_EnableLoadStoreDebugMode;
- // OpenGL ES 3.1+
- SerializedProperty m_RequireES31;
+ // OpenGL ES 3.1+ - m_RequireES31 removed. Android minspec raised to 3.1 (GDRIV-4724)
SerializedProperty m_RequireES31AEP;
SerializedProperty m_RequireES32;
@@ -551,6 +551,7 @@ internal static void SyncEditors(BuildTarget target)
List m_SectionAnimators = new List(kNumberGUISections);
readonly AnimBool m_ShowDefaultIsNativeResolution = new AnimBool();
readonly AnimBool m_ShowResolution = new AnimBool();
+ [NoAutoStaticsCleanup]
private static Texture2D s_WarningIcon;
// Preset check
@@ -558,6 +559,7 @@ internal static void SyncEditors(BuildTarget target)
bool hasPresetWindowClosed = false;
// True when user has modified auto graphics API setting in the current session
+ [AutoStaticsCleanupOnCodeReload] // session flag: persisting true would incorrectly trigger the restart prompt after reload
private static bool isAutoGraphicsAPITouched = false;
///
@@ -606,7 +608,8 @@ public SerializedProperty FindPropertyAssert(string name)
return property;
}
- private static List s_activeEditors = new List();
+ [AutoStaticsCleanupOnCodeReload] // list of active Editor instances; must clear on code reload
+ private static readonly List s_activeEditors = new();
void OnEnable()
{
s_activeEditors.Add(this);
@@ -735,7 +738,6 @@ void OnEnable()
m_EnableFrameTimingStats = FindPropertyAssert("enableFrameTimingStats");
m_EnableOpenGLProfilerGPURecorders = FindPropertyAssert("enableOpenGLProfilerGPURecorders");
- m_RequireES31 = FindPropertyAssert("openGLRequireES31");
m_RequireES31AEP = FindPropertyAssert("openGLRequireES31AEP");
m_RequireES32 = FindPropertyAssert("openGLRequireES32");
@@ -1487,11 +1489,7 @@ static private GraphicsDeviceType RecommendedGraphicsDeviceTypeFromDeprecated(Bu
// Checks if the GraphicsDeviceType is experimental
static private bool IsGraphicsDeviceTypeExperimental(BuildTarget target, GraphicsDeviceType graphicsDeviceType)
{
- switch (graphicsDeviceType)
- {
- case GraphicsDeviceType.WebGPU: return true;
- default: return false;
- }
+ return false;
}
// Converts a GraphicsDeviceType to a string, along with visual modifiers for given target platform
@@ -1815,7 +1813,6 @@ void OpenGLES31OptionsGUI(BuildTargetGroup targetGroup, BuildTarget targetPlatfo
if (!hasMinES3)
return;
- EditorGUILayout.PropertyField(m_RequireES31, SettingsContent.require31);
EditorGUILayout.PropertyField(m_RequireES31AEP, SettingsContent.requireAEP);
EditorGUILayout.PropertyField(m_RequireES32, SettingsContent.require32);
}
@@ -2080,7 +2077,8 @@ void GraphicsAPIsGUI(BuildTargetGroup targetGroup, BuildTarget target)
//
// This information might be useful for users that use the color gamut APIs,
// we could expose it somehow
- private static Dictionary> s_SupportedColorGamuts =
+ [NoAutoStaticsCleanup] // static platform capability table — content never changes
+ private static readonly ImmutableDictionary> s_SupportedColorGamuts =
new Dictionary>
{
{ BuildTargetGroup.Standalone, new List { ColorGamut.sRGB, ColorGamut.DisplayP3 } },
@@ -2088,7 +2086,7 @@ void GraphicsAPIsGUI(BuildTargetGroup targetGroup, BuildTarget target)
{ BuildTargetGroup.tvOS, new List { ColorGamut.sRGB, ColorGamut.DisplayP3 } },
{ BuildTargetGroup.VisionOS, new List { ColorGamut.sRGB, ColorGamut.DisplayP3 } },
{ BuildTargetGroup.Android, new List {ColorGamut.sRGB, ColorGamut.DisplayP3 } }
- };
+ }.ToImmutableDictionary();
private static bool IsColorGamutSupportedOnTargetGroup(BuildTargetGroup targetGroup, ColorGamut gamut)
{
@@ -3090,6 +3088,7 @@ private bool VirtualTexturingInvalidGfxAPI(BuildTarget target, bool checkEditor)
return !supportedAPI;
}
+ [AutoStaticsCleanupOnCodeReload] // lazy cache keyed on IBuildTarget; interface may be implemented by user code
private static readonly Dictionary virtualTexturingUnsupportedAPIContents = new();
void ShowWarningIfVirtualTexturingUnsupportedByAPI(IBuildTarget buildTarget, bool checkEditor)
@@ -4234,8 +4233,8 @@ private void OtherSectionOptimizationGUI(BuildPlatform platform)
EditorGUILayout.Space();
}
- static ManagedStrippingLevel[] mono_levels = new ManagedStrippingLevel[] { ManagedStrippingLevel.Disabled, ManagedStrippingLevel.Minimal, ManagedStrippingLevel.Low, ManagedStrippingLevel.Medium, ManagedStrippingLevel.High };
- static ManagedStrippingLevel[] il2cpp_levels = new ManagedStrippingLevel[] { ManagedStrippingLevel.Minimal, ManagedStrippingLevel.Low, ManagedStrippingLevel.Medium, ManagedStrippingLevel.High };
+ static readonly ManagedStrippingLevel[] mono_levels = new ManagedStrippingLevel[] { ManagedStrippingLevel.Disabled, ManagedStrippingLevel.Minimal, ManagedStrippingLevel.Low, ManagedStrippingLevel.Medium, ManagedStrippingLevel.High };
+ static readonly ManagedStrippingLevel[] il2cpp_levels = new ManagedStrippingLevel[] { ManagedStrippingLevel.Minimal, ManagedStrippingLevel.Low, ManagedStrippingLevel.Medium, ManagedStrippingLevel.High };
// stripping levels vary based on scripting backend
private ManagedStrippingLevel[] GetAvailableManagedStrippingLevels(ScriptingImplementation backend)
{
@@ -4249,21 +4248,17 @@ private ManagedStrippingLevel[] GetAvailableManagedStrippingLevels(ScriptingImpl
}
}
- static Il2CppCompilerConfiguration[] m_Il2cppCompilerConfigurations;
+ static readonly Il2CppCompilerConfiguration[] m_Il2cppCompilerConfigurations = new Il2CppCompilerConfiguration[]
+ {
+ Il2CppCompilerConfiguration.Debug,
+ Il2CppCompilerConfiguration.Release,
+ Il2CppCompilerConfiguration.Master,
+ };
+ [NoAutoStaticsCleanup] // lazy-built GUIContent names; rebuilt on next GUI call if null
static GUIContent[] m_Il2cppCompilerConfigurationNames;
private Il2CppCompilerConfiguration[] GetIl2CppCompilerConfigurations()
{
- if (m_Il2cppCompilerConfigurations == null)
- {
- m_Il2cppCompilerConfigurations = new Il2CppCompilerConfiguration[]
- {
- Il2CppCompilerConfiguration.Debug,
- Il2CppCompilerConfiguration.Release,
- Il2CppCompilerConfiguration.Master,
- };
- }
-
return m_Il2cppCompilerConfigurations;
}
@@ -4281,14 +4276,12 @@ private GUIContent[] GetIl2CppCompilerConfigurationNames()
return m_Il2cppCompilerConfigurationNames;
}
- static Il2CppStacktraceInformation[] m_Il2cppStacktraceOptions;
+ static readonly Il2CppStacktraceInformation[] m_Il2cppStacktraceOptions = (Il2CppStacktraceInformation[])Enum.GetValues(typeof(Il2CppStacktraceInformation));
+ [NoAutoStaticsCleanup] // lazy-built GUIContent names; rebuilt on next GUI call if null
static GUIContent[] m_Il2cppStacktraceOptionNames;
private Il2CppStacktraceInformation[] GetIl2CppStacktraceOptions()
{
- if (m_Il2cppStacktraceOptions == null)
- m_Il2cppStacktraceOptions = (Il2CppStacktraceInformation[])Enum.GetValues(typeof(Il2CppStacktraceInformation));
-
return m_Il2cppStacktraceOptions;
}
@@ -4377,8 +4370,11 @@ private void OtherSectionCaptureLogsGUI(NamedBuildTarget namedBuildTarget)
EditorGUILayout.Space();
}
+ [NoAutoStaticsCleanup] // lazy-built GUIContent cache; rebuilt on next GUI call if null
private static Dictionary m_NiceApiCompatibilityLevelNames;
+ [NoAutoStaticsCleanup] // lazy-built GUIContent cache; rebuilt on next GUI call if null
private static Dictionary m_NiceEditorAssembliesCompatibilityLevelNames;
+ [NoAutoStaticsCleanup] // lazy-built GUIContent cache; rebuilt on next GUI call if null
private static Dictionary m_NiceManagedStrippingLevelNames;
private static GUIContent[] GetGUIContentsForValues(Dictionary contents, T[] values)
diff --git a/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsSplashScreenEditor.cs b/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsSplashScreenEditor.cs
index 2a1aeece97..5057cb905f 100644
--- a/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsSplashScreenEditor.cs
+++ b/Editor/Mono/Inspector/PlayerSettingsEditor/PlayerSettingsSplashScreenEditor.cs
@@ -9,6 +9,7 @@
using UnityEngine;
using UnityEditor.Build;
using UnityEngine.Events;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
@@ -50,7 +51,9 @@ internal partial class PlayerSettingsSplashScreenEditor
static readonly Color32 k_DarkOnLightBgColor = new Color32(204, 204, 204, 255);// #CCCCCC
static readonly Color32 k_LightOnDarkBgColor = new Color32(35, 31, 32, 255);
+ [NoAutoStaticsCleanup] // sprite is a built-in asset reference; safe to persist across code reload
static Sprite s_UnityLogoLight; // We use this version as a placeholder when the logo is in the list.
+ [NoAutoStaticsCleanup] // sprite is a built-in asset reference; safe to persist across code reload
static Sprite s_UnityLogoDark;
readonly AnimBool m_ShowAnimationControlsAnimator = new AnimBool();
diff --git a/Editor/Mono/Inspector/PlayerSettingsEditor/WebTemplateManagerBase.cs b/Editor/Mono/Inspector/PlayerSettingsEditor/WebTemplateManagerBase.cs
index 3c3b7dd24d..74f7bdb5a1 100644
--- a/Editor/Mono/Inspector/PlayerSettingsEditor/WebTemplateManagerBase.cs
+++ b/Editor/Mono/Inspector/PlayerSettingsEditor/WebTemplateManagerBase.cs
@@ -5,6 +5,7 @@
using UnityEngine;
using System.Collections.Generic;
using System.IO;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
@@ -16,6 +17,7 @@ class Styles
public GUIStyle thumbnailLabel = "IN ThumbnailSelection";
}
+ [NoAutoStaticsCleanup] // lazy Styles instance; rebuilt on next GUI call if null
private static Styles s_Styles;
private WebTemplate[] m_Templates = null;
diff --git a/Editor/Mono/Inspector/QualitySettingsEditor.cs b/Editor/Mono/Inspector/QualitySettingsEditor.cs
index 53f5051372..b7d347ae0c 100644
--- a/Editor/Mono/Inspector/QualitySettingsEditor.cs
+++ b/Editor/Mono/Inspector/QualitySettingsEditor.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.RegularExpressions;
using Unity.Collections;
using UnityEditor.Build;
using UnityEditor.Build.Profile;
@@ -185,6 +186,18 @@ internal class Styles
// Inspected quality level (separate from current active level)
private int? m_InspectedQualityLevelField;
+
+ // Helper method to elide text for menu items
+ private static string ElideText(string text, int maxLength = 50, string ellipsis = "...")
+ {
+ if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
+ return text;
+
+ if (maxLength <= ellipsis.Length)
+ return ellipsis;
+
+ return text.Substring(0, maxLength - ellipsis.Length) + ellipsis;
+ }
private const string kInspectedQualityLevelPrefKey = "QualitySettingsEditor.selectedLevel";
private int selectedLevel
@@ -309,7 +322,6 @@ public void OnDestroy()
// Clear cached property references for current quality level
m_CurrentQualityProperty = null;
m_CurrentSettings = null;
- m_NameProperty = null;
m_PixelLightCountProperty = null;
m_ShadowsProperty = null;
m_ShadowResolutionProperty = null;
@@ -382,7 +394,11 @@ public override VisualElement CreateInspectorGUI()
// Bind to SerializedObject once when the panel is attached
m_CurrentRoot.RegisterCallback(evt =>
{
+ m_QualitySettings.Update();
m_CurrentRoot.Bind(m_QualitySettings);
+
+ // Tracks changes to the quality settings, including external changes (e.g., from preset application, reset, file modifications) to ensure the UI stays in sync with the data
+ m_CurrentRoot.TrackSerializedObjectValue(m_QualitySettings, OnSerializedObjectChanged);
});
var titleBar = new ProjectSettingsTitleBar("Quality");
@@ -423,6 +439,31 @@ internal void Dispose()
m_CurrentRoot = null;
}
+ private void OnSerializedObjectChanged(SerializedObject obj)
+ {
+ RebindQualityLevelsListView();
+ EnsureValidQualityLevelNames();
+ OnQualityLevelSelectionChanged(m_QualitySettingsProperty);
+ }
+
+ private void EnsureValidQualityLevelNames()
+ {
+ // Ensure all quality levels have valid names, assign default ones if empty
+ for (int i = 0; i < m_QualitySettingsProperty.arraySize; i++)
+ {
+ var levelProp = m_QualitySettingsProperty.GetArrayElementAtIndex(i);
+ var nameProp = levelProp.FindPropertyRelative("name");
+ if (string.IsNullOrEmpty(nameProp.stringValue))
+ {
+ nameProp.stringValue = "Level " + (i + 1);
+ BuildProfileModuleUtil.RenameQualityLevelInAllProfiles(string.Empty, nameProp.stringValue);
+ QualitySettings.OnActiveQualityLevelRenamed(string.Empty, nameProp.stringValue);
+ }
+ }
+
+ m_QualitySettings.ApplyModifiedProperties();
+ }
+
private void OnActiveQualityLevelChanged(int previousLevel, int currentLevel)
{
if (!m_IsEditingQualitySettings)
@@ -502,7 +543,6 @@ private void SetDefaultQualityForPlatforms(Dictionary platformDefau
private void RebuildPlatformDefaultsCache()
{
m_QualitySettings.Update();
-
m_CachedPlatformDefaults.Clear();
foreach (SerializedProperty prop in m_PerPlatformDefaultQualityProperty)
@@ -514,9 +554,12 @@ private void RebuildPlatformDefaultsCache()
// UITK Table Building Methods
private void BuildQualityLevelTable()
{
- m_QualityTableContainer = new VisualElement();
- m_QualityTableContainer.name = "QualityLevelTable";
+ m_QualityTableContainer = new VisualElement
+ {
+ name = "QualityLevelTable"
+ };
m_QualityTableContainer.AddToClassList("quality-table");
+ m_CurrentRoot.Add(m_QualityTableContainer);
// Build header
var header = BuildHeaderRow();
@@ -534,40 +577,42 @@ private void BuildQualityLevelTable()
virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
selectionType = SelectionType.Single,
showAddRemoveFooter = true,
-
- // Set up makeItem callback
makeItem = MakeQualityLevelItem,
-
- // Set up bindItem callback
bindItem = BindQualityLevelItem,
-
- // Set up unbindItem callback
- unbindItem = UnbindQualityLevelItem
+ unbindItem = UnbindQualityLevelItem,
+ destroyItem = DestroyQualityLevelItem
};
m_QualityLevelsListView.itemIndexChanged += OnQualityLevelItemIndexChanged;
-
- // Handle selection changes
m_QualityLevelsListView.selectionChanged += OnQualityLevelSelectionChanged;
-
- // Handle add/remove
m_QualityLevelsListView.onAdd += OnAddQualityLevel;
m_QualityLevelsListView.onRemove += OnRemoveQualityLevel;
m_QualityTableContainer.Add(m_QualityLevelsListView);
- // Align levels label when the table is attached to panel
- m_QualityTableContainer.RegisterCallback(evt => AlignLevelsLabelWidth());
-
- m_QualityLevelsListView.RegisterCallback(evt =>
+ m_QualityTableContainer.RegisterCallback(evt =>
{
- m_QualityLevelsListView.BindProperty(m_QualitySettingsProperty);
- UpdateInspectedLevelSelection();
+ // Make sure that the binding system has assigned the itemsSource before trying to align the levels label (which depends on the first row's toggle position)
+ // This will only be triggered the first GeometryChangedEvent, as after rebinding the ListView, the itemsSource will be assigned and this block will be skipped in subsequent GeometryChangedEvents
+ // Same functionality as AttachToPanelEvent, but guarantees that the ListView has been fully initialized and bound before trying to align the levels label
+ if (m_QualityLevelsListView.itemsSource == null)
+ {
+ RebindQualityLevelsListView();
+ RefreshQualityUI();
+ }
+
+ AlignLevelsLabelWidth();
});
+ }
- m_QualityLevelsListView.TrackPropertyValue(m_QualitySettingsProperty, OnQualityLevelSelectionChanged);
+ private void RebindQualityLevelsListView()
+ {
+ if (m_QualityLevelsListView == null)
+ return;
- m_CurrentRoot.Add(m_QualityTableContainer);
+ m_QualitySettings.Update();
+ m_QualityLevelsListView.Unbind();
+ m_QualityLevelsListView.BindProperty(m_QualitySettingsProperty);
}
private int AdjustIndexAfterReorder(int currentIndex, int oldIndex, int newIndex)
@@ -622,8 +667,6 @@ private void UpdateInspectedLevelSelection()
private void OnQualityLevelItemIndexChanged(int oldIndex, int newIndex)
{
- // Update cached platform defaults to reflect the reorder
- // When a quality level moves, we need to adjust all default indices
var platformKeys = new List(m_CachedPlatformDefaults.Keys);
foreach (var key in platformKeys)
{
@@ -631,28 +674,24 @@ private void OnQualityLevelItemIndexChanged(int oldIndex, int newIndex)
m_CachedPlatformDefaults[key] = AdjustIndexAfterReorder(value, oldIndex, newIndex);
}
- // Write back to serialized property
SetDefaultQualityForPlatforms(m_CachedPlatformDefaults);
m_QualitySettings.ApplyModifiedProperties();
- // Update the current active quality level if it was moved
var currentActiveLevel = GetCurrentTargetQualityLevel();
int newActiveLevel = AdjustIndexAfterReorder(currentActiveLevel, oldIndex, newIndex);
- // Apply the new active level if it changed
if (newActiveLevel != currentActiveLevel)
{
SetCurrentTargetQualityLevel(newActiveLevel);
}
- // Update the inspected level to follow the reordered item
selectedLevel = AdjustIndexAfterReorder(selectedLevel, oldIndex, newIndex);
- // Update selection
UpdateInspectedLevelSelection();
- // Refresh UI to show updated defaults and current tag
- m_QualityLevelsListView?.RefreshItems();
+ RebindQualityLevelsListView();
+
+ RefreshQualityUI();
}
private void OnQualityLevelSelectionChanged(SerializedProperty property)
@@ -733,7 +772,8 @@ private VisualElement BuildHeaderRow()
string qualityName = currentSettings[i].m_Name;
bool isSelected = (i == currentDefault);
- menu.AddItem(new GUIContent(qualityName), isSelected, () =>
+ var content = new GUIContent(ElideText(qualityName));
+ menu.AddItem(content, isSelected, () =>
{
var defs = GetDefaultQualityForPlatforms();
defs[capturedPlatformName] = qualityIndex;
@@ -812,16 +852,8 @@ private VisualElement MakeQualityLevelItem()
name = "QualityName"
};
nameField.AddToClassList("quality-table__quality-name");
- nameField.isDelayed = true; // Only trigger callback on focus lost or Enter, not every keystroke
-
- nameField.RegisterValueChangedCallback(evt =>
- {
- if (evt.target is TextField field && field.userData is int index)
- {
- OnQualityNameChanged(index, evt.previousValue, evt.newValue);
- }
- });
-
+ nameField.isDelayed = true;
+ nameField.RegisterValueChangedCallback(OnQualityNameChanged);
row.Add(nameField);
for (int i = 0; i < m_ValidPlatforms.Count; i++)
@@ -844,6 +876,16 @@ private VisualElement MakeQualityLevelItem()
return row;
}
+ private void DestroyQualityLevelItem(VisualElement element)
+ {
+ // Unregister callbacks to avoid memory leaks (in case elements are reused by ListView)
+ var nameField = element.Q("QualityName");
+ if (nameField != null)
+ {
+ nameField.UnregisterValueChangedCallback(OnQualityNameChanged);
+ }
+ }
+
private void BindQualityLevelItem(VisualElement element, int index)
{
if (index < 0 || index >= m_QualitySettingsProperty.arraySize)
@@ -859,17 +901,16 @@ private void BindQualityLevelItem(VisualElement element, int index)
var currentTag = element.Q("CurrentQualityLevelTag");
var spacer = element.Q("CurrentQualityLevelSpacer");
var nameField = element.Q("QualityName");
- bool showTag = m_IsEditingQualitySettings && isCurrentLevel;
// Toggle between showing the "Current" tag or the spacer
if (currentTag != null)
{
- currentTag.style.display = showTag ? DisplayStyle.Flex : DisplayStyle.None;
+ currentTag.style.display = isCurrentLevel ? DisplayStyle.Flex : DisplayStyle.None;
}
if (spacer != null)
{
- spacer.style.display = showTag ? DisplayStyle.None : DisplayStyle.Flex;
+ spacer.style.display = isCurrentLevel ? DisplayStyle.None : DisplayStyle.Flex;
}
if (nameField != null)
@@ -924,13 +965,14 @@ private void UnbindQualityLevelItem(VisualElement element, int index)
}
}
-
- // Event Handlers
private void OnQualityLevelSelectionChanged(IEnumerable selectedItems)
{
if (m_QualityLevelsListView == null)
return;
+ // Commit any pending edits before we change selection
+ CommitPendingNameEdits();
+
int selectedIndex = m_QualityLevelsListView.selectedIndex;
if (selectedIndex >= 0)
{
@@ -943,8 +985,45 @@ private void OnQualityLevelSelectionChanged(IEnumerable selectedItems)
}
}
+ private void CommitPendingNameEdits()
+ {
+ if (m_QualityLevelsListView == null)
+ return;
+
+ m_QualitySettings.Update();
+
+ m_QualityLevelsListView.Query(className: "quality-table__quality-name")
+ .ForEach(field =>
+ {
+ if (!field.hasFocus
+ || field.userData is not int qualityIndex
+ || qualityIndex < 0
+ || qualityIndex >= m_QualitySettingsProperty.arraySize)
+ return;
+
+ var qualityProperty = m_QualitySettingsProperty.GetArrayElementAtIndex(qualityIndex);
+ var nameProperty = qualityProperty.FindPropertyRelative("name");
+ var serializedValue = nameProperty.stringValue;
+
+ // Get the current value from the text field's underlying text element as it is the source of truth for the displayed value,
+ // rather than the TextField's value which may not have been updated yet due to delayed binding and event handling
+ var textElement = field.Q();
+ var fieldValue = textElement != null ? textElement.text : field.value;
+
+ if (!string.Equals(serializedValue, fieldValue, StringComparison.Ordinal))
+ {
+ OnQualityNameCommitted(field, qualityProperty, qualityIndex, serializedValue, fieldValue);
+ }
+
+ // Blurring forces the delayed TextField to commit its uncommitted text to field.value and synchronously trigger the ChangeEvent
+ field.Blur();
+ });
+ }
+
private void OnPlatformToggleChanged(int qualityIndex, string platformName, bool enabled)
{
+ m_QualitySettings.Update();
+
// Validate index
if (qualityIndex < 0 || qualityIndex >= m_QualitySettingsProperty.arraySize)
return;
@@ -997,29 +1076,42 @@ private void RemovePlatformFromArray(SerializedProperty arrayProp, string platfo
m_QualitySettings.ApplyModifiedProperties();
}
- private void OnQualityNameChanged(int qualityIndex, string previousName, string newName)
+ // Regex to remove invalid characters from quality names. Allows only alphanumeric characters, spaces, underscores, and hyphens.
+ private static readonly Regex k_InvalidCharsRegex = new Regex(@"[^a-zA-Z0-9 _-]", RegexOptions.Compiled);
+
+ private static string CleanQualityName(string input) => input == null ? string.Empty : k_InvalidCharsRegex.Replace(input, string.Empty);
+
+ private void OnQualityNameChanged(ChangeEvent evt)
{
- if (qualityIndex < 0 || qualityIndex >= m_QualitySettingsProperty.arraySize)
+ if (evt.currentTarget is not TextField field || field.userData is not int qualityIndex || qualityIndex < 0 || qualityIndex >= m_QualitySettingsProperty.arraySize)
return;
- var qualityProperty = m_QualitySettingsProperty.GetArrayElementAtIndex(qualityIndex);
+ OnQualityNameCommitted(
+ field,
+ m_QualitySettingsProperty.GetArrayElementAtIndex(qualityIndex),
+ qualityIndex,
+ evt.previousValue,
+ evt.newValue);
+
+ evt.StopPropagation();
+ }
+
+ private void OnQualityNameCommitted(TextField field, SerializedProperty qualityProperty, int qualityIndex, string previousName, string newName)
+ {
+ newName = CleanQualityName(newName);
+
var nameProperty = qualityProperty.FindPropertyRelative("name");
if (nameProperty != null)
{
- // Handle empty names
- if (string.IsNullOrEmpty(newName))
- newName = "Level " + qualityIndex;
-
- // Update the property and track the new name for pending rename
- nameProperty.stringValue = newName;
- m_QualitySettings.ApplyModifiedProperties();
-
if (m_IsEditingQualitySettings)
{
BuildProfileModuleUtil.RenameQualityLevelInAllProfiles(previousName, newName);
QualitySettings.OnActiveQualityLevelRenamed(previousName, newName);
}
+ nameProperty.stringValue = newName;
+ m_QualitySettings.ApplyModifiedProperties();
+
// Update the header if the renamed level is currently inspected
if (qualityIndex == selectedLevel)
UpdateQualityLevelHeader();
@@ -1028,19 +1120,23 @@ private void OnQualityNameChanged(int qualityIndex, string previousName, string
private void OnAddQualityLevel(BaseListView listView)
{
- int index = m_QualitySettingsProperty.arraySize;
+ CommitPendingNameEdits();
- m_QualitySettingsProperty.InsertArrayElementAtIndex(index);
+ m_QualitySettings.Update();
- var qualityProperty = m_QualitySettingsProperty.GetArrayElementAtIndex(index);
+ int newItemIndex = m_QualitySettingsProperty.arraySize;
+
+ m_QualitySettingsProperty.InsertArrayElementAtIndex(newItemIndex);
+
+ var qualityProperty = m_QualitySettingsProperty.GetArrayElementAtIndex(newItemIndex);
var nameProperty = qualityProperty.FindPropertyRelative("name");
if (nameProperty != null)
- nameProperty.stringValue = "Level " + (index + 1);
+ nameProperty.stringValue = string.Empty;
m_QualitySettings.ApplyModifiedProperties();
// Update inspected level to point to the new item
- selectedLevel = index + 1;
+ selectedLevel = newItemIndex;
// Select the newly added level
UpdateInspectedLevelSelection();
@@ -1051,6 +1147,8 @@ private void OnAddQualityLevel(BaseListView listView)
private void OnRemoveQualityLevel(BaseListView listView)
{
+ CommitPendingNameEdits();
+
m_QualitySettings.Update();
if (m_QualitySettingsProperty.arraySize <= 1)
@@ -1101,18 +1199,15 @@ private void OnRemoveQualityLevel(BaseListView listView)
// Update selection and cached properties
UpdateInspectedLevelSelection();
- // Adjust the current active quality level
- if (m_IsEditingQualitySettings)
- {
- var currentActive = GetCurrentTargetQualityLevel();
- int newActiveIndex = AdjustIndexAfterDeletion(currentActive, index);
+ var currentActive = GetCurrentTargetQualityLevel();
+ int newActiveIndex = AdjustIndexAfterDeletion(currentActive, index);
- // Apply the adjustment if needed
- if (newActiveIndex != currentActive)
- {
- SetCurrentTargetQualityLevel(newActiveIndex);
+ // Apply the adjustment if needed
+ if (newActiveIndex != currentActive)
+ {
+ SetCurrentTargetQualityLevel(newActiveIndex);
+ if (m_IsEditingQualitySettings)
QualitySettings.OnActiveQualityLevelChanged(index, newActiveIndex);
- }
}
// Refresh UI to update the "Current" tag
@@ -1139,7 +1234,11 @@ private void BuildQualityLevelHeader()
name = "QualityLevelNameLabel",
style = {
unityFontStyleAndWeight = FontStyle.Bold,
- fontSize = 18
+ fontSize = 18,
+ maxWidth = 500,
+ overflow = Overflow.Hidden,
+ textOverflow = TextOverflow.Ellipsis,
+ whiteSpace = WhiteSpace.NoWrap
}
};
m_QualityLevelHeader.Add(m_QualityLevelNameLabel);
@@ -1199,22 +1298,11 @@ private void UpdateQualityLevelHeader()
m_QualityLevelNameLabel.text = levelName;
- // Show/hide "Current" tag and button based on whether we're editing QualitySettings
- // and whether this is the current active level
- if (m_IsEditingQualitySettings)
- {
- var currentActiveLevel = GetCurrentTargetQualityLevel();
- bool isCurrentLevel = (selectedLevel == currentActiveLevel);
+ var currentActiveLevel = GetCurrentTargetQualityLevel();
+ bool isCurrentLevel = (selectedLevel == currentActiveLevel);
- m_QualityLevelCurrentTag.style.display = isCurrentLevel ? DisplayStyle.Flex : DisplayStyle.None;
- m_SetCurrentButton.style.display = isCurrentLevel ? DisplayStyle.None : DisplayStyle.Flex;
- }
- else
- {
- // For presets, hide both tag and button
- m_QualityLevelCurrentTag.style.display = DisplayStyle.None;
- m_SetCurrentButton.style.display = DisplayStyle.None;
- }
+ m_QualityLevelCurrentTag.style.display = isCurrentLevel ? DisplayStyle.Flex : DisplayStyle.None;
+ m_SetCurrentButton.style.display = isCurrentLevel ? DisplayStyle.None : DisplayStyle.Flex;
}
}
@@ -1261,6 +1349,7 @@ private void DrawQualityLevelDetailsIMGUI()
private void RefreshQualityUI()
{
+ m_QualitySettings.Update();
UpdateQualityLevelHeader();
m_QualityLevelsListView?.RefreshItems();
m_QualityDetailsContainer?.MarkDirtyRepaint();
@@ -1748,7 +1837,6 @@ private void ShowAffectedBuildProfileInformation()
// Cached SerializedProperty references for current quality level
private SerializedProperty m_CurrentSettings;
- private SerializedProperty m_NameProperty;
private SerializedProperty m_PixelLightCountProperty;
private SerializedProperty m_ShadowsProperty;
private SerializedProperty m_ShadowResolutionProperty;
@@ -1801,7 +1889,6 @@ private void UpdateCachedProperties(int selectedLevel)
{
m_CurrentSettings = m_QualitySettingsProperty.GetArrayElementAtIndex(selectedLevel);
- m_NameProperty = m_CurrentSettings.FindPropertyRelative("name");
m_PixelLightCountProperty = m_CurrentSettings.FindPropertyRelative("pixelLightCount");
m_ShadowsProperty = m_CurrentSettings.FindPropertyRelative("shadows");
m_ShadowResolutionProperty = m_CurrentSettings.FindPropertyRelative("shadowResolution");
diff --git a/Editor/Mono/Inspector/RectHandles.cs b/Editor/Mono/Inspector/RectHandles.cs
index f6ebcf7423..f590f0bccd 100644
--- a/Editor/Mono/Inspector/RectHandles.cs
+++ b/Editor/Mono/Inspector/RectHandles.cs
@@ -251,6 +251,7 @@ public static void RenderRectWithShadow(bool active, params Vector3[] corners)
}
static Vector3[] s_TempVectors = System.Array.Empty();
+
public static void DrawPolyLineWithShadow(Color shadowColor, Vector2 screenOffset, params Vector3[] points)
{
Camera cam = Camera.current;
@@ -261,7 +262,7 @@ public static void DrawPolyLineWithShadow(Color shadowColor, Vector2 screenOffse
s_TempVectors = new Vector3[points.Length];
for (int i = 0; i < points.Length; i++)
- s_TempVectors[i] = cam.ScreenToWorldPoint(cam.WorldToScreenPoint(points[i]) + (Vector3)screenOffset);
+ s_TempVectors[i] = HandleUtility.WorldPointWithScreenOffset(cam, points[i], screenOffset);
Color oldColor = Handles.color;
@@ -287,8 +288,8 @@ public static void DrawDottedLineWithShadow(Color shadowColor, Vector2 screenOff
shadowColor.a = shadowColor.a * oldColor.a;
Handles.color = shadowColor;
Handles.DrawDottedLine(
- cam.ScreenToWorldPoint(cam.WorldToScreenPoint(p1) + (Vector3)screenOffset),
- cam.ScreenToWorldPoint(cam.WorldToScreenPoint(p2) + (Vector3)screenOffset), screenSpaceSize);
+ HandleUtility.WorldPointWithScreenOffset(cam, p1, screenOffset),
+ HandleUtility.WorldPointWithScreenOffset(cam, p2, screenOffset), screenSpaceSize);
// line itself
Handles.color = oldColor;
diff --git a/Editor/Mono/Inspector/ShaderImporterInspector.cs b/Editor/Mono/Inspector/ShaderImporterInspector.cs
index 6efaf17da4..4a51def943 100644
--- a/Editor/Mono/Inspector/ShaderImporterInspector.cs
+++ b/Editor/Mono/Inspector/ShaderImporterInspector.cs
@@ -113,6 +113,8 @@ protected override void InitializeExtraDataInstance(Object extraTarget, int targ
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_Properties = extraDataSerializedObject.FindProperty("m_Properties");
}
diff --git a/Editor/Mono/Inspector/SpriteRendererEditor.cs b/Editor/Mono/Inspector/SpriteRendererEditor.cs
index d107362c0e..1706e24a33 100644
--- a/Editor/Mono/Inspector/SpriteRendererEditor.cs
+++ b/Editor/Mono/Inspector/SpriteRendererEditor.cs
@@ -86,11 +86,8 @@ public override void OnInspectorGUI()
serializedObject.Update();
EditorGUILayout.PropertyField(m_Sprite, Styles.spriteLabel);
- using (new EditorGUI.DisabledScope(m_Sprite.objectReferenceValue == null || m_Sprite.hasMultipleDifferentValues))
- {
- if(SpriteUtilityWindow.DoOpenSpriteEditorWindowUI())
- SpriteUtilityWindow.ShowSpriteEditorWindow(target);
- }
+ if(SpriteUtilityWindow.DoOpenSpriteEditorWindowUI(m_Sprite.objectReferenceValue != null && !m_Sprite.hasMultipleDifferentValues))
+ SpriteUtilityWindow.ShowSpriteEditorWindow(target);
GUILayout.Space(5);
OnBlendShapeUI();
diff --git a/Editor/Mono/Inspector/TimelineControl.cs b/Editor/Mono/Inspector/TimelineControl.cs
index d0524ccaeb..ad28270d95 100644
--- a/Editor/Mono/Inspector/TimelineControl.cs
+++ b/Editor/Mono/Inspector/TimelineControl.cs
@@ -630,8 +630,13 @@ public bool DoTimeline(Rect timeRect)
GUIContent srcContent = EditorGUIUtility.TempContent(SrcName);
- // draw src Loop
- int srcLoopCount = srcLoop ? (1 + (int)((transStop - srcRect.xMin) / (srcRect.xMax - srcRect.xMin))) : 1;
+ // Draw src Loop
+ // Calculations are using time values to avoid imprecision when scaling pixel values.
+ float rightThumbTimeOffset = m_TimeArea.PixelToTime(m_RightThumbOffset, r) - m_TimeArea.PixelToTime(r.x, r);
+ int srcLoopCount = srcLoop
+ ? (1 + (int)((TransitionStopTime + rightThumbTimeOffset - SrcStartTime)
+ / (SrcStopTime - SrcStartTime)))
+ : 1;
Rect loopRect = srcRect;
if (srcRect.width < 10) // if smaller than 10 pixel, group
{
@@ -672,7 +677,14 @@ public bool DoTimeline(Rect timeRect)
GUIContent dstContent = EditorGUIUtility.TempContent(DstName);
- int dstLoopCount = dstLoop ? (1 + (int)((transStop - dstRect.xMin) / (dstRect.xMax - dstRect.xMin))) : 1;
+
+ // Draw dst Loop
+ // Calculations are using time values to avoid imprecision when scaling pixel values.
+ float dstTimeOffset = m_TimeArea.PixelToTime(m_DstDragOffset, r) - m_TimeArea.PixelToTime(r.x, r);
+ int dstLoopCount = dstLoop
+ ? (1 + (int)((TransitionStopTime + rightThumbTimeOffset - DstStartTime - dstTimeOffset)
+ / (DstStopTime - DstStartTime)))
+ : 1;
loopRect = dstRect;
if (dstRect.width < 10) // if smaller than 10 pixel, group
{
diff --git a/Editor/Mono/Inspector/TrailRendererEditor.cs b/Editor/Mono/Inspector/TrailRendererEditor.cs
index 46395c3754..535b520593 100644
--- a/Editor/Mono/Inspector/TrailRendererEditor.cs
+++ b/Editor/Mono/Inspector/TrailRendererEditor.cs
@@ -9,12 +9,13 @@
using UnityEditor.Overlays;
using UnityEditor.ShortcutManagement;
using UnityEngine;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
[CustomEditor(typeof(TrailRenderer))]
[CanEditMultipleObjects]
- internal class TrailRendererInspector : RendererEditorBase
+ internal partial class TrailRendererInspector : RendererEditorBase
{
private class Styles
{
@@ -60,13 +61,21 @@ private enum PreviewShape
private const string k_PreviewShape = "TrailPreviewShape";
private const string k_PreviewShapeSize = "TrailPreviewShapeSize";
+ [AutoStaticsCleanupOnCodeReload] // active inspector tracking list
private static LinkedList s_Inspectors = new LinkedList();
+ [NoAutoStaticsCleanup] // editor UI state flag
private static bool s_PreviewIsPlaying;
+ [NoAutoStaticsCleanup] // editor UI state flag
private static bool s_PreviewIsPaused;
+ [NoAutoStaticsCleanup] // editor state; no user refs
private static float s_PreviewMovementSpeed;
+ [NoAutoStaticsCleanup] // editor state; no user refs
private static float s_PreviewTimeScale;
+ [NoAutoStaticsCleanup] // editor UI state flag
private static bool s_PreviewShowBounds;
+ [NoAutoStaticsCleanup] // editor infrastructure; no user refs
private static PreviewShape s_PreviewShape;
+ [NoAutoStaticsCleanup] // editor state; no user refs
private static float s_PreviewShapeSize;
private Vector3? m_PreviewBackupPosition;
private bool m_PreviewIsFirstMove;
@@ -108,13 +117,17 @@ private static Event CreateCommandEvent(string commandName)
return new Event { type = EventType.ExecuteCommand, commandName = "TrailRenderer/" + commandName };
}
+ [AutoStaticsCleanupOnCodeReload] // cached command event; infrastructure
private static Event s_PlayEvent;
+ [AutoStaticsCleanupOnCodeReload] // cached command event; infrastructure
private static Event s_StopEvent;
+ [AutoStaticsCleanupOnCodeReload] // cached command event; infrastructure
private static Event s_RestartEvent;
+ [AutoStaticsCleanupOnCodeReload] // cached command event; infrastructure
private static Event s_ShowBoundsEvent;
- private static PrefColor s_BoundsColor = new PrefColor("Trail Renderer/Bounds", 1.0f, 235.0f / 255.0f, 4.0f / 255.0f, 1.0f);
- private static PrefColor s_GizmoColor = new PrefColor("Trail Renderer/Shape Gizmos", 148f / 255f, 229f / 255f, 1f, 0.9f);
+ private static readonly PrefColor s_BoundsColor = new PrefColor("Trail Renderer/Bounds", 1.0f, 235.0f / 255.0f, 4.0f / 255.0f, 1.0f);
+ private static readonly PrefColor s_GizmoColor = new PrefColor("Trail Renderer/Shape Gizmos", 148f / 255f, 229f / 255f, 1f, 0.9f);
private static void DispatchShortcutEvent(Event evt)
{
diff --git a/Editor/Mono/Inspector/VersionControlSettingsInspector.cs b/Editor/Mono/Inspector/VersionControlSettingsInspector.cs
index 570227eb61..c29ab1fd55 100644
--- a/Editor/Mono/Inspector/VersionControlSettingsInspector.cs
+++ b/Editor/Mono/Inspector/VersionControlSettingsInspector.cs
@@ -38,6 +38,9 @@ class Styles
public static GUIContent overwriteFailedCheckoutAssets =
new GUIContent("Overwrite Failed Checkout Assets",
"When on, assets that can not be checked out will get saved anyway.");
+ public static GUIContent autoRevertUnchangedFiles =
+ new GUIContent("Auto Revert Unchanged Files",
+ "Automatically revert files that were checked out during asset import but have no actual content changes. Uses server-side hash verification, so files with real changes are preserved.");
public static GUIContent overlayIcons = new GUIContent("Overlay Icons",
"Should version control status icons be shown.");
public static GUIContent projectOverlayIcons = new GUIContent("Project Window",
@@ -404,6 +407,10 @@ public override void OnInspectorGUI()
Styles.overwriteFailedCheckoutAssets, EditorUserSettings.overwriteFailedCheckoutAssets);
}
+ if (Provider.hasRevertUnchangedSupport)
+ EditorUserSettings.autoRevertUnchangedFiles = EditorGUILayout.Toggle(
+ Styles.autoRevertUnchangedFiles, EditorUserSettings.autoRevertUnchangedFiles);
+
EditorUserSettings.semanticMergeMode = (SemanticMergeMode)EditorGUILayout.Popup(Styles.smartMerge,
(int)EditorUserSettings.semanticMergeMode, semanticMergePopupList);
diff --git a/Editor/Mono/Inspector/VisualElements/RenderingLayerMaskField.cs b/Editor/Mono/Inspector/VisualElements/RenderingLayerMaskField.cs
index 96cc49f6d0..e361e1315c 100644
--- a/Editor/Mono/Inspector/VisualElements/RenderingLayerMaskField.cs
+++ b/Editor/Mono/Inspector/VisualElements/RenderingLayerMaskField.cs
@@ -12,7 +12,7 @@ namespace UnityEditor.UIElements
///
/// A RenderingLayerMaskField editor.
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class RenderingLayerMaskField : BaseMaskField
{
///
diff --git a/Editor/Mono/Loadable/LoadableObjectIdEditorUtility.cs b/Editor/Mono/Loadable/LoadableObjectIdEditorUtility.cs
index bbab9186ca..8c28e5519e 100644
--- a/Editor/Mono/Loadable/LoadableObjectIdEditorUtility.cs
+++ b/Editor/Mono/Loadable/LoadableObjectIdEditorUtility.cs
@@ -14,12 +14,19 @@ namespace UnityEditor
/// Editor utilities for creating and converting values when authoring content.
///
///
- /// is the low-level reference type used by `Loadable{T}` for on-demand object loading.
+ /// is the low-level reference type used by for on-demand object loading.
/// These methods convert between live instances and serialized loadable object IDs for content directory builds.
+ ///
+ /// Use this utility to populate a root asset or a field from a script. It is also useful when
+ /// implementing custom Editor UI for ScriptableObjects or MonoBehaviours that contain or
+ /// fields.
///
///
///
///
+ ///
+ ///
+ ///
[NativeHeader("Editor/Src/Utility/LoadableObjectIdEditorUtility.bindings.h")]
[VisibleToOtherModules]
public static class LoadableObjectIdEditorUtility
@@ -40,7 +47,7 @@ internal static string GetLoadableObjectIdTooltip()
/// The to deconstruct.
/// The GUID of the asset file containing the referenced object.
/// The local file identifier of the object within the asset.
- /// The file identifier type indicating whether this is a source asset, primary artifact, or non-asset reference.
+ /// The indicating whether this is a source asset, primary artifact, or non-asset reference.
///
/// true if the LoadableObjectId represents an asset reference and all components were successfully retrieved;
/// false if the LoadableObjectId is a runtime handle.
diff --git a/Editor/Mono/Loadable/LoadableSceneIdEditorUtility.cs b/Editor/Mono/Loadable/LoadableSceneIdEditorUtility.cs
index 966b572c01..e61234ea39 100644
--- a/Editor/Mono/Loadable/LoadableSceneIdEditorUtility.cs
+++ b/Editor/Mono/Loadable/LoadableSceneIdEditorUtility.cs
@@ -14,6 +14,8 @@ namespace UnityEditor
/// Utility class to create objects for use in the Editor. A typical use would be to populate fields of type
/// on a class derived from (or ).
///
+ ///
+ ///
public static class LoadableSceneIdEditorUtility
{
///
diff --git a/Editor/Mono/Menu.bindings.cs b/Editor/Mono/Menu.bindings.cs
index ac998fc6ae..20be9e16db 100644
--- a/Editor/Mono/Menu.bindings.cs
+++ b/Editor/Mono/Menu.bindings.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Bindings;
using UnityEngine.Scripting;
using System.Runtime.InteropServices;
@@ -31,8 +32,9 @@ public ScriptingMenuItem(string path, int priority = -1, bool isSeparator = fals
}
[NativeHeader("Editor/Src/MenuController.h")]
- public sealed class Menu
+ public sealed partial class Menu
{
+ [AutoStaticsCleanupOnCodeReload]
[VisibleToOtherModules("UnityEditor.ShaderFoundryModule")]
internal static event Action menuChanged;
diff --git a/Editor/Mono/Modules/BeeBuildPostprocessor.cs b/Editor/Mono/Modules/BeeBuildPostprocessor.cs
index c46df32a7f..620a7b02e8 100644
--- a/Editor/Mono/Modules/BeeBuildPostprocessor.cs
+++ b/Editor/Mono/Modules/BeeBuildPostprocessor.cs
@@ -125,6 +125,11 @@ public virtual void UpdateBootConfig(BuildTarget target, BootConfigData config,
[RequiredByNativeCode]
static void EndProfile() => UnityBeeDriverProfilerSession.Finish();
+ // The session only writes its output file when it is finished, so consumers of that file can use
+ // this to check they are not running too early.
+ [RequiredByNativeCode]
+ static bool IsProfilerSessionActive() => UnityBeeDriverProfilerSession.PerformingPlayerBuild;
+
[RequiredByNativeCode]
static void BeginBuildSection(string name) => UnityBeeDriverProfilerSession.BeginSection(name);
diff --git a/Editor/Mono/Modules/DefaultBuildProfileExtension.cs b/Editor/Mono/Modules/DefaultBuildProfileExtension.cs
index 970163d185..963cedb486 100644
--- a/Editor/Mono/Modules/DefaultBuildProfileExtension.cs
+++ b/Editor/Mono/Modules/DefaultBuildProfileExtension.cs
@@ -41,6 +41,13 @@ internal abstract class DefaultBuildProfileExtension : IBuildProfileExtension
EditorGUIUtility.TrTextContent("LZ4HC"),
};
static readonly GUIContent k_InstallInBuildFolder = EditorGUIUtility.TrTextContent("Install into source code 'build' folder", "Install into source checkout 'build' folder, for debugging with source code");
+
+ // Development build infobox
+ static readonly string developmentBuildInfoBoxText = string.Format(L10n.Tr("Starting in Unity 6.6, part of the Development Build setting has been split out into the Managed Code Variant player setting. Click here for more information."), kDevelopmentBuildInfoBoxUrl);
+ static readonly string developmentBuildInfoBoxButtonText = L10n.Tr("Dismiss");
+ const string kDevelopmentBuildInfoBoxUrl = "https://discussions.unity.com/t/-/1721546";
+ const string kDevelopmentBuildInfoBoxPreferenceKey = "developmentBuildInfoBoxDismissed";
+
protected SerializedProperty m_Development = null;
SerializedProperty m_ConnectProfiler = null;
SerializedProperty m_BuildWithDeepProfilingSupport = null;
@@ -130,10 +137,64 @@ public VisualElement CreateSettingsGUI(
settingsGUI.Add(platformSettingsGUI);
if (BuildPlayerWindow.WillDrawMultiplayerBuildOptions())
settingsGUI.Add(CreateMultiplayerSettingsGUI(serializedObject.targetObject as BuildProfile));
+
+ AddDevelopmentBuildCheckbox(settingsGUI, serializedObject, rootProperty);
+
settingsGUI.Add(commonSettingsGUI);
return settingsGUI;
}
+ private void AddDevelopmentBuildCheckbox(VisualElement parent, SerializedObject serializedObject, SerializedProperty rootProperty)
+ {
+ void AssignPropertiesIfNeeded()
+ {
+ if (m_Development == null || !m_Development.isValid)
+ m_Development = FindPlatformSettingsPropertyAssert(rootProperty, "m_Development");
+ }
+
+ AssignPropertiesIfNeeded();
+
+ parent.Add(new IMGUIContainer(() =>
+ {
+ if (serializedObject == null || !serializedObject.isValid || !ShouldDrawDevelopmentPlayerCheckbox())
+ return;
+
+ var oldLabelWidth = EditorGUIUtility.labelWidth;
+ EditorGUIUtility.labelWidth = labelWidth;
+ try
+ {
+ serializedObject.UpdateIfRequiredOrScript();
+ AssignPropertiesIfNeeded();
+ ShowDevelopmentPlayerCheckbox();
+ serializedObject.ApplyModifiedProperties();
+ }
+ finally
+ {
+ EditorGUIUtility.labelWidth = oldLabelWidth;
+ }
+ }));
+
+ if (!EditorPrefs.GetBool(kDevelopmentBuildInfoBoxPreferenceKey, false))
+ parent.Add(CreateDevelopmentBuildInfoBox());
+ }
+
+ private VisualElement CreateDevelopmentBuildInfoBox()
+ {
+ var infoBox = new HelpBox();
+ infoBox.style.flexDirection = FlexDirection.Row;
+
+ infoBox.buttonText = developmentBuildInfoBoxButtonText;
+ infoBox.onButtonClicked += () =>
+ {
+ infoBox.style.display = DisplayStyle.None;
+ EditorPrefs.SetBool(kDevelopmentBuildInfoBoxPreferenceKey, true);
+ };
+
+ infoBox.text = developmentBuildInfoBoxText;
+
+ return infoBox;
+ }
+
public virtual VisualElement CreatePlatformSettingsGUI(
SerializedObject serializedObject, SerializedProperty rootProperty, BuildProfileWorkflowState workflowState)
{
@@ -238,11 +299,6 @@ VisualElement CreateMultiplayerSettingsGUI(BuildProfile profile)
public void ShowCommonBuildOptions(BuildProfileWorkflowState workflowState)
{
- if (ShouldDrawDevelopmentPlayerCheckbox())
- {
- ShowDevelopmentPlayerCheckbox();
- }
-
if (ShouldDrawLinkTimeOptimization())
{
ShowLinkTimeOptimization();
diff --git a/Editor/Mono/Modules/ModuleManager.cs b/Editor/Mono/Modules/ModuleManager.cs
index cb58ce1b36..9103b61b2c 100644
--- a/Editor/Mono/Modules/ModuleManager.cs
+++ b/Editor/Mono/Modules/ModuleManager.cs
@@ -223,7 +223,7 @@ private static void RegisterPlatformSupportModules()
var (buildTarget, _) = BuildTargetDiscovery.GetBuildTargetAndSubtargetFromGUID(platformSupportModule.PlatformBuildTarget.Guid);
if (BuildTargetDiscovery.IsStandalonePlatform(buildTarget))
{
- if (BuildTargetDiscovery.TryGetServerGUIDFromBuildTarget(NamedBuildTarget.Server, buildTarget, out var serverGuid))
+ if (BuildTargetDiscovery.TryGetBaseServerGUIDFromBuildTarget(NamedBuildTarget.Server, buildTarget, out var serverGuid))
{
if (BuildTargetDiscovery.BuildPlatformIsInstalled(serverGuid))
s_PlatformModulesByGuid.Add(serverGuid, platformSupportModule);
diff --git a/Editor/Mono/Modules/PlatformSDK/SDKPlatformInfo.cs b/Editor/Mono/Modules/PlatformSDK/SDKPlatformInfo.cs
index 6c4e138e2e..2980f3009a 100644
--- a/Editor/Mono/Modules/PlatformSDK/SDKPlatformInfo.cs
+++ b/Editor/Mono/Modules/PlatformSDK/SDKPlatformInfo.cs
@@ -37,4 +37,6 @@ internal class SDKPlatformInfo
public PlatformPackageList internalPackages;
public PlatformPackageList partnerPackages;
public SDKPlatformFlags flags;
+ public bool isDeprecated;
+ public string deprecationMessage;
}
diff --git a/Editor/Mono/ObjectListArea.cs b/Editor/Mono/ObjectListArea.cs
index d56fadcd11..d20b8378cc 100644
--- a/Editor/Mono/ObjectListArea.cs
+++ b/Editor/Mono/ObjectListArea.cs
@@ -106,6 +106,7 @@ static class Styles
Vector2 m_LastScrollPosition = new Vector2(0, 0);
+ float m_FramedSelectionItemY = float.NaN;
double LastScrollTime = 0;
internal Texture m_SelectedObjectIcon = null;
@@ -461,6 +462,15 @@ public int numItemsDisplayed
get { return m_LocalAssets.ItemCount; }
}
+ internal bool IsSelectionFramed()
+ {
+ int idx = GetSelectedAssetIdx();
+ if (idx < 0)
+ return false;
+ Rect r = m_LocalAssets.m_Grid.CalcRect(idx, 0f);
+ return r.yMin >= m_State.m_ScrollPosition.y && r.yMax <= m_State.m_ScrollPosition.y + m_TotalRect.height;
+ }
+
bool ObjectsHaveThumbnails(HierarchyType type, SearchFilter searchFilter, SearchService.SearchSessionOptions searchSessionOptions)
{
// Check if we have any built-ins, if so we have thumbs since all builtins have thumbs
@@ -652,6 +662,8 @@ public void SetSelection(EntityId[] selectedInstanceIDs, bool doubleClicked)
public void InitSelection(EntityId[] selectedInstanceIDs)
{
+ m_FramedSelectionItemY = float.NaN;
+
// Note that selectedInstanceIDs can be gameObjects
m_State.m_SelectedInstanceIDs = new List(selectedInstanceIDs);
@@ -958,6 +970,24 @@ public bool Frame(EntityId entityId, bool frame, bool ping)
return false;
}
+ // The grid re-lays out over the first passes after opening; re-frame whenever the selection's position changes.
+ internal void KeepSelectionFramed()
+ {
+ if (m_State.m_SelectedInstanceIDs.Count == 0 || m_State.m_SelectedInstanceIDs[0] == EntityId.None)
+ return;
+
+ int index = m_LocalAssets.IndexOf(m_State.m_SelectedInstanceIDs[0]);
+ if (index == -1)
+ return; // items not populated yet
+
+ float itemY = m_LocalAssets.m_Grid.CalcRect(index, 0f).y;
+ if (Mathf.Approximately(itemY, m_FramedSelectionItemY))
+ return; // layout unchanged since the selection was last framed
+
+ m_FramedSelectionItemY = itemY;
+ Frame(m_State.m_SelectedInstanceIDs[0], true, false);
+ }
+
int GetSelectedAssetIdx()
{
// Find index of selection
diff --git a/Editor/Mono/ObjectListGroup.cs b/Editor/Mono/ObjectListGroup.cs
index 53608591a9..620456789d 100644
--- a/Editor/Mono/ObjectListGroup.cs
+++ b/Editor/Mono/ObjectListGroup.cs
@@ -13,7 +13,8 @@ internal partial class ObjectListArea
*/
protected abstract class Group
{
- readonly protected float kGroupSeparatorHeight = EditorStyles.toolbar.fixedHeight;
+ // Evaluating it lazily avoids touching the style off-GUI.
+ protected float kGroupSeparatorHeight => EditorStyles.toolbar.fixedHeight;
protected string m_GroupSeparatorTitle;
protected static int[] s_Empty;
diff --git a/Editor/Mono/ObjectSelector.cs b/Editor/Mono/ObjectSelector.cs
index 2d7bbd9e0b..607e1533bb 100644
--- a/Editor/Mono/ObjectSelector.cs
+++ b/Editor/Mono/ObjectSelector.cs
@@ -108,6 +108,11 @@ public static void GraphKeyboardFocus(ObjectSelector os)
os?.GrabKeyboardFocus();
}
+ public static bool IsSelectionFramed(ObjectSelector os)
+ {
+ return os?.m_ListArea?.IsSelectionFramed() ?? false;
+ }
+
public static void NotifySelectionChanged(ObjectSelector os, UnityObject selectedObject, bool exitGUI)
{
os?.NotifySelectionChanged(selectedObject, exitGUI);
@@ -182,6 +187,7 @@ public static void SetSelection(ObjectSelector os, EntityId[] selection, bool do
bool m_SelectionCancelled;
bool m_PreventSetSelectionOnClose;
+ bool m_FrameInitialSelection;
EntityId m_LastSelectedInstanceId = EntityId.None;
readonly SearchService.ObjectSelectorSearchSessionHandler m_SearchSessionHandler = new SearchService.ObjectSelectorSearchSessionHandler();
readonly SearchSessionOptions m_LegacySearchSessionOptions = new SearchSessionOptions { legacyOnly = true };
@@ -336,6 +342,10 @@ void SetSelectedInstanceID(EntityId entityId)
void OnEnable()
{
hideFlags = HideFlags.DontSave;
+
+ // UUM-144436: Cancel and close the picker before the reload instead of trying to resurrect a broken window.
+ AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
+
m_ShowOverlapPreview.valueChanged.AddListener(Repaint);
m_ShowOverlapPreview.speed = 1.5f;
m_ShowWidePreview.valueChanged.AddListener(Repaint);
@@ -371,6 +381,8 @@ void OnEnable()
[UsedImplicitly]
void OnDisable()
{
+ AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
+
NotifySelectorClosed(false);
if (m_ListArea != null)
m_StartGridSize.value = m_ListArea.gridSize;
@@ -404,7 +416,14 @@ void ListAreaItemSelectedCallback(bool doubleClicked)
}
else
{
- NotifySelectionChanged(true);
+ // Do not pass exitGUI:true here. The notification may queue a UI Toolkit event
+ // (e.g. assigning a UITK ObjectField value queues a ChangeEvent) that opens a
+ // native modal dialog. Exiting the GUI throws an ExitGUIException that flushes
+ // that queued event during its unwind, which wedges the selector because of the
+ // native modal dialog. Letting the GUI complete normally flushes the queued event
+ // through the regular dispatcher path instead. (Same as the engine-override path,
+ // which also notifies with exitGUI: false.)
+ NotifySelectionChanged(false);
}
}
@@ -628,6 +647,7 @@ void SharedShow(UnityObject obj, RequiredTypeList typeList, UnityObject objectBe
SetSelectedInstanceID(obj?.GetEntityId() ?? EntityId.None);
m_SelectionCancelled = false;
m_PreventSetSelectionOnClose = false;
+ m_FrameInitialSelection = false;
m_ShowNoneItem = showNoneItem;
m_OnObjectSelectorClosed = onObjectSelectorClosed;
@@ -778,7 +798,7 @@ void SharedShow(UnityObject obj, RequiredTypeList typeList, UnityObject objectBe
InitIfNeeded();
m_ListArea.InitSelection(new[] { initialSelection });
if (initialSelection != EntityId.None)
- m_ListArea.Frame(initialSelection, true, false);
+ m_FrameInitialSelection = true;
}
InvokeWindowShown(this);
@@ -823,7 +843,8 @@ void CreateAndSetTreeView(ObjectTreeForSelector.TreeSelectorData data)
void TreeViewSelection(TreeViewItem item)
{
SetSelectedInstanceID(GetInternalSelectedInstanceID());
- NotifySelectionChanged(true);
+ // See ListAreaItemSelectedCallback for why we notify with exitGUI: false.
+ NotifySelectionChanged(false);
}
// Grid Section
@@ -1083,6 +1104,18 @@ void HandleKeyboard()
GUI.changed = true;
}
+ void OnBeforeAssemblyReload()
+ {
+ Undo.RevertAllDownToGroup(m_ModalUndoGroup);
+ m_ListArea?.InitSelection(Array.Empty());
+ m_ObjectTreeWithSearch.Clear();
+ SetSelectedInstanceID(EntityId.None);
+ m_SelectionCancelled = true;
+ m_EditedProperty = null;
+
+ Close();
+ }
+
internal void Cancel()
{
// Undo changes we have done in the ObjectSelector
@@ -1113,6 +1146,14 @@ void OnDestroy()
void OnGUIHandler()
{
+ // Must run before the list area consumes the event, otherwise its type is already EventType.Used.
+ if (m_FrameInitialSelection)
+ {
+ var type = Event.current.type;
+ if (type == EventType.MouseDown || type == EventType.MouseDrag || type == EventType.ScrollWheel || type == EventType.KeyDown)
+ m_FrameInitialSelection = false;
+ }
+
HandleKeyboard();
m_Position = m_ImGUIContainer.worldBound;
@@ -1321,6 +1362,9 @@ void OnObjectGridGUI()
GUI.EndGroup();
+ if (m_FrameInitialSelection && Event.current.type == EventType.Layout && m_Position.height > 0)
+ m_ListArea.KeepSelectionFramed();
+
// overlay preview resize widget
GUI.Label(new Rect(m_Position.width * .5f - 16, m_Position.height - m_PreviewSize + 2, 32, Styles.bottomResize.fixedHeight), GUIContent.none, Styles.bottomResize);
}
diff --git a/Editor/Mono/Overlays/OverlayPrefs.cs b/Editor/Mono/Overlays/OverlayPrefs.cs
index 18e5fc9338..edda3b951f 100644
--- a/Editor/Mono/Overlays/OverlayPrefs.cs
+++ b/Editor/Mono/Overlays/OverlayPrefs.cs
@@ -71,6 +71,8 @@ class WindowSettings
public DynamicPanelBehavior dynamicPanelBehavior { get; private set; }
public bool allowDynamicPanelBehaviorChanges { get; private set; }
+ public string canvasSelector { get; }
+
public bool enabled
{
get => EditorPrefs.GetBool($"OverlayEnabled.{type.AssemblyQualifiedName}", true);
@@ -92,6 +94,7 @@ public WindowSettings(Type type, Color defaultBackgroundColor, DynamicPanelBehav
defaultBackgroundColor.g,
defaultBackgroundColor.b,
defaultBackgroundColor.a);
+ canvasSelector = "unity-overlay-canvas-" + type.Name.ToLowerInvariant();
}
}
@@ -181,7 +184,10 @@ public static DynamicPanelBehavior GetDefaultDynamicPanelBehavior(Type windowTyp
internal static string GetPreferenceCanvasClass(Type windowType)
{
- return $"unity-overlay-canvas-{windowType.Name.ToLowerInvariant()}";
+ if (instance.m_Windows.TryGetValue(windowType, out var settings))
+ return settings.canvasSelector;
+
+ return "unity-overlay-canvas-" + windowType.Name.ToLowerInvariant();
}
public static IEnumerable GetSupportedWindowTypes()
@@ -214,10 +220,14 @@ static void RequestStyleSheetRebuild()
public static string BuildOverlayStylesheetString()
{
var sb = new StringBuilder();
+ var invariant = System.Globalization.CultureInfo.InvariantCulture;
foreach (var type in GetSupportedWindowTypes())
{
- var color = GetBackgroundColor(type);
+ if (!instance.m_Windows.TryGetValue(type, out var settings))
+ continue;
+
+ Color color = settings.backgroundColor;
int r = Mathf.RoundToInt(color.r * 255f);
int g = Mathf.RoundToInt(color.g * 255f);
@@ -225,12 +235,20 @@ public static string BuildOverlayStylesheetString()
float a = color.a;
float popupAlpha = Mathf.Max(a, k_DefaultPopupAlpha); // Ensure we use the most opaque version for the popup
- var selector = GetPreferenceCanvasClass(type);
-
- sb.AppendLine($".{selector} {{");
- sb.AppendLine($" --unity-overlay-background-color: rgba({r}, {g}, {b}, {a.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)});");
- sb.AppendLine($" --unity-overlay-popup-background-color: rgba({r}, {g}, {b}, {popupAlpha.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)});");
- sb.AppendLine("}");
+ sb.Append('.').Append(settings.canvasSelector).Append(" {").AppendLine();
+ sb.Append(" --unity-overlay-background-color: rgba(")
+ .Append(r).Append(", ")
+ .Append(g).Append(", ")
+ .Append(b).Append(", ")
+ .Append(a.ToString("0.###", invariant))
+ .Append(");").AppendLine();
+ sb.Append(" --unity-overlay-popup-background-color: rgba(")
+ .Append(r).Append(", ")
+ .Append(g).Append(", ")
+ .Append(b).Append(", ")
+ .Append(popupAlpha.ToString("0.###", invariant))
+ .Append(");").AppendLine();
+ sb.Append('}').AppendLine();
}
return sb.ToString();
diff --git a/Editor/Mono/Overlays/OverlayPresetManager.cs b/Editor/Mono/Overlays/OverlayPresetManager.cs
index 663ce48488..802a9de06d 100644
--- a/Editor/Mono/Overlays/OverlayPresetManager.cs
+++ b/Editor/Mono/Overlays/OverlayPresetManager.cs
@@ -207,6 +207,9 @@ public static bool Exists(Type windowType, string presetName)
public static IOverlayPreset GetDefaultPreset(Type windowType)
{
+ if (windowType == typeof(MainToolbarWindow))
+ return new UnityOnlyToolbarPreset();
+
if (TryGetPreset(windowType, defaultPresetName, out OverlayPreset preset))
return preset;
diff --git a/Editor/Mono/Overlays/OverlayUtilities.cs b/Editor/Mono/Overlays/OverlayUtilities.cs
index 95247ff60d..b9eab0b411 100644
--- a/Editor/Mono/Overlays/OverlayUtilities.cs
+++ b/Editor/Mono/Overlays/OverlayUtilities.cs
@@ -180,7 +180,7 @@ internal static List GetOverlaysForType(Type windowType, Func;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditor
{
- internal class FrameDebuggerWindow : EditorWindow
+ internal partial class FrameDebuggerWindow : EditorWindow
{
// Serialized
[SerializeField] private float m_TreeWidth = FrameDebuggerStyles.Window.k_MinTreeWidth;
@@ -34,6 +35,7 @@ internal class FrameDebuggerWindow : EditorWindow
private FrameDebuggerEventDetailsView m_EventDetailsView;
private FrameDebuggerToolbarView m_Toolbar;
+ [NoAutoStaticsCleanup] // lifecycle managed by ReleaseGraphicsBuffers (OnDestroy/playModeStateChanged) and [OnCodeUnloading] for code-reload safety
private static Lazy m_ShadingRateLut =
new Lazy(CreateShadingRateLutGraphicsBuffer);
internal static GraphicsBuffer shadingRateLut => m_ShadingRateLut.Value;
@@ -68,8 +70,15 @@ private static void ReleaseGraphicsBuffers()
new Lazy(CreateShadingRateLutGraphicsBuffer);
}
+ [OnCodeUnloading]
+ private static void OnCodeUnloading()
+ {
+ ReleaseGraphicsBuffers();
+ }
+
// Statics
- private static List s_FrameDebuggers = new List();
+ [AutoStaticsCleanupOnCodeReload]
+ private static readonly List s_FrameDebuggers = new();
// Constants
diff --git a/Editor/Mono/PerformanceTools/FrameDebuggerEventDisplayData.cs b/Editor/Mono/PerformanceTools/FrameDebuggerEventDisplayData.cs
index 6b4ef9b038..13cce4d724 100644
--- a/Editor/Mono/PerformanceTools/FrameDebuggerEventDisplayData.cs
+++ b/Editor/Mono/PerformanceTools/FrameDebuggerEventDisplayData.cs
@@ -12,6 +12,7 @@
using UnityEngine.Experimental.Rendering;
using UnityEditor;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditorInternal.FrameDebuggerInternal
{
@@ -228,6 +229,7 @@ internal string detailsCopyString
}
}
+ [NoAutoStaticsCleanup] // reusable scratch buffer, content discarded after each use via Clear()
private static StringBuilder s_DetailsBuilder = new StringBuilder(4096);
private string m_Details;
internal string details
diff --git a/Editor/Mono/PerformanceTools/FrameDebuggerHelper.cs b/Editor/Mono/PerformanceTools/FrameDebuggerHelper.cs
index 09ddd96143..a139cf6e60 100644
--- a/Editor/Mono/PerformanceTools/FrameDebuggerHelper.cs
+++ b/Editor/Mono/PerformanceTools/FrameDebuggerHelper.cs
@@ -8,6 +8,7 @@
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using UnityEditor.Rendering;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditorInternal.FrameDebuggerInternal
{
@@ -56,8 +57,11 @@ internal static Material shadingRateImageMaterial
// Private Static Variables
+ [NoAutoStaticsCleanup] // lazy material cache; lazy-init pattern, no code-reload-sensitive state
private static Material s_Material = null;
+ [NoAutoStaticsCleanup] // lazy material cache for VRS visualization; same pattern as s_Material
private static Material s_ShadingRateImageMaterial = null;
+ [NoAutoStaticsCleanup] // reusable scratch buffer for building shader stage strings
private static StringBuilder s_StringBuilder = new StringBuilder();
// Functions
@@ -68,15 +72,15 @@ private struct ShaderPropertyIDs
internal const string _MSAA_8 = "_MSAA_8";
internal const string _TEX2DARRAY = "_TEX2DARRAY";
internal const string _CUBEMAP = "_CUBEMAP";
- internal static int _Levels = Shader.PropertyToID("_Levels");
- internal static int _MainTex = Shader.PropertyToID("_MainTex");
- internal static int _MainTexDepth = Shader.PropertyToID("_MainTexDepth");
- internal static int _Channels = Shader.PropertyToID("_Channels");
- internal static int _ShouldYFlip = Shader.PropertyToID("_ShouldYFlip");
- internal static int _UndoOutputSRGB = Shader.PropertyToID("_UndoOutputSRGB");
- internal static int _MainTexWidth = Shader.PropertyToID("_MainTexWidth");
- internal static int _MainTexHeight = Shader.PropertyToID("_MainTexHeight");
- internal static int _VisualizationLut = Shader.PropertyToID("_VisualizationLut");
+ internal static readonly int _Levels = Shader.PropertyToID("_Levels");
+ internal static readonly int _MainTex = Shader.PropertyToID("_MainTex");
+ internal static readonly int _MainTexDepth = Shader.PropertyToID("_MainTexDepth");
+ internal static readonly int _Channels = Shader.PropertyToID("_Channels");
+ internal static readonly int _ShouldYFlip = Shader.PropertyToID("_ShouldYFlip");
+ internal static readonly int _UndoOutputSRGB = Shader.PropertyToID("_UndoOutputSRGB");
+ internal static readonly int _MainTexWidth = Shader.PropertyToID("_MainTexWidth");
+ internal static readonly int _MainTexHeight = Shader.PropertyToID("_MainTexHeight");
+ internal static readonly int _VisualizationLut = Shader.PropertyToID("_VisualizationLut");
}
internal static void BlitToRenderTexture(
diff --git a/Editor/Mono/PerformanceTools/FrameDebuggerStyles.cs b/Editor/Mono/PerformanceTools/FrameDebuggerStyles.cs
index 1e248945ac..dde6830636 100644
--- a/Editor/Mono/PerformanceTools/FrameDebuggerStyles.cs
+++ b/Editor/Mono/PerformanceTools/FrameDebuggerStyles.cs
@@ -4,6 +4,7 @@
using UnityEditor;
using UnityEngine;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEditorInternal.FrameDebuggerInternal
{
@@ -158,10 +159,11 @@ internal struct EventDetails
internal const string k_IntFormat = "d";
internal const string k_NotAvailable = "-";
- internal static string s_DashesString = new string('-', 30);
- internal static string s_EqualsString = new string('=', 30);
+ internal static readonly string s_DashesString = new string('-', 30);
+ internal static readonly string s_EqualsString = new string('=', 30);
// Cached width for two-column format label (matches k_TwoColumnFormat first column width of 22 chars)
+ [NoAutoStaticsCleanup] // cached measurement of a fixed GUIStyle; style persists across code reload so value remains valid
private static float s_TwoColumnLabelWidth = -1f;
internal static float TwoColumnLabelWidth
{
@@ -315,6 +317,7 @@ internal DetailsSectionInfo(GUIContent header, string prefsKey, bool defaultOpen
}
}
+ [NoAutoStaticsCleanup] // immutable config array; GUIContent entries and string keys are code-reload-safe
internal static readonly DetailsSectionInfo[] s_DetailsSections = new DetailsSectionInfo[]
{
new DetailsSectionInfo(
@@ -354,6 +357,7 @@ internal DetailsSectionInfo(GUIContent header, string prefsKey, bool defaultOpen
),
};
internal static readonly GUIContent s_NotAvailableText = EditorGUIUtility.TrTextContent(k_NotAvailable);
+ [NoAutoStaticsCleanup] // programmatic Texture2D persists across incremental code reload; lifecycle managed by FrameDebuggerStyles.OnDisable
internal static Texture2D s_RenderTargetMeshBackgroundTexture = null;
internal static readonly string[] s_BatchBreakCauses = FrameDebuggerUtility.GetBatchBreakCauseStrings();
}
diff --git a/Editor/Mono/PlayerSettings.bindings.cs b/Editor/Mono/PlayerSettings.bindings.cs
index 46739a95ef..79ce542e0e 100644
--- a/Editor/Mono/PlayerSettings.bindings.cs
+++ b/Editor/Mono/PlayerSettings.bindings.cs
@@ -746,15 +746,8 @@ public static bool singlePassStereoRendering
public static extern bool useFlipModelSwapchain { get; set; }
- [NativeProperty(TargetType = TargetType.Field)]
- public static extern bool openGLRequireES31
- {
- [StaticAccessor("GetPlayerSettings().GetEditorOnly()")]
- get;
-
- [StaticAccessor("GetPlayerSettings().GetEditorOnlyForUpdate()")]
- set;
- }
+ [Obsolete("OpenGL ES 3.1 is now the Android minimum supported version; this setting has no effect.", false)]
+ public static bool openGLRequireES31 { get { return true; } set { } }
[NativeProperty(TargetType = TargetType.Field)]
public static extern bool openGLRequireES31AEP
@@ -1265,6 +1258,10 @@ public static void SetManagedStrippingLevel(NamedBuildTarget buildTarget, Manage
private static extern ApiCompatibilityLevel GetApiCompatibilityLevelInternal(string buildTargetName);
public static ApiCompatibilityLevel GetApiCompatibilityLevel(NamedBuildTarget buildTarget) => GetApiCompatibilityLevelInternal(buildTarget.TargetName);
+ [StaticAccessor("PlayerSettingsBindings", StaticAccessorType.DoubleColon)]
+ [NativeMethod("GetApiCompatibilityLevel_Internal", ThrowsException = true)]
+ internal static extern ApiCompatibilityLevel GetApiCompatibilityLevel_Internal(PlayerSettings instance, string buildTargetName);
+
[StaticAccessor("GetPlayerSettings().GetEditorOnlyForUpdate()")]
[NativeMethod("SetApiCompatibilityLevel", ThrowsException = true)]
private static extern void SetApiCompatibilityLevelInternal(string buildTargetName, ApiCompatibilityLevel value);
diff --git a/Editor/Mono/PlayerSettingsAndroid.bindings.cs b/Editor/Mono/PlayerSettingsAndroid.bindings.cs
index d9b45ca5ee..a7c2d24e3e 100644
--- a/Editor/Mono/PlayerSettingsAndroid.bindings.cs
+++ b/Editor/Mono/PlayerSettingsAndroid.bindings.cs
@@ -121,6 +121,9 @@ public enum AndroidSdkVersions
// Android 16.0, API level 36
AndroidApiLevel36 = 36,
+
+ // Android 17.0, API level 37.0
+ AndroidApiLevel37 = 37,
}
// Preferred application install location
diff --git a/Editor/Mono/Prefabs/PrefabOverrides/PrefabOverridesTreeView.cs b/Editor/Mono/Prefabs/PrefabOverrides/PrefabOverridesTreeView.cs
index 8a7d922abf..36e72f8cf3 100644
--- a/Editor/Mono/Prefabs/PrefabOverrides/PrefabOverridesTreeView.cs
+++ b/Editor/Mono/Prefabs/PrefabOverrides/PrefabOverridesTreeView.cs
@@ -688,10 +688,18 @@ public override VisualElement CreateGUI()
if (m_Modification != null && m_Source != null && m_Instance != null)
root.AddToClassList(Styles.dualViewClass);
- root.Add(CreateHeader());
+ var header = CreateHeader();
+ root.Add(header);
if (m_Modification != null)
- root.Add(CreateComparisonView());
+ {
+ var comparisonView = CreateComparisonView();
+ root.Add(comparisonView);
+
+ // Header is outside the ScrollView, so compensate for the scrollbar width to keep splits aligned. (UUM-125064)
+ comparisonView.contentViewport.RegisterCallback(evt =>
+ header.style.paddingRight = Mathf.Max(0f, comparisonView.layout.width - evt.newRect.width));
+ }
return root;
}
@@ -735,7 +743,7 @@ VisualElement CreateHeaderButtons()
return container;
}
- VisualElement CreateComparisonView()
+ ScrollView CreateComparisonView()
{
var comparisonView = new ScrollView
{
diff --git a/Editor/Mono/PreferencesWindow/PreferencesSettingsProviders.cs b/Editor/Mono/PreferencesWindow/PreferencesSettingsProviders.cs
index 97259fa780..ceae52dd1a 100644
--- a/Editor/Mono/PreferencesWindow/PreferencesSettingsProviders.cs
+++ b/Editor/Mono/PreferencesWindow/PreferencesSettingsProviders.cs
@@ -12,6 +12,7 @@
using JetBrains.Annotations;
using Unity.CodeEditor;
using Unity.Collections;
+using UnityEngine.Analytics;
using UnityEngine.UIElements;
using UnityEditor.Experimental;
using UnityEditor.SceneManagement;
@@ -73,9 +74,7 @@ class GeneralProperties
public static readonly GUIContent editorTextGeneration = EditorGUIUtility.TrTextContent("Editor Text Generator Type");
public static readonly GUIContent editorSkin = EditorGUIUtility.TrTextContent("Editor Theme","Choose between light and dark themes for the Editor.\nThe Editor theme cannot be changed while in Play mode.");
public static readonly GUIContent[] editorSkinOptions = { EditorGUIUtility.TrTextContent("Light"), EditorGUIUtility.TrTextContent("Dark") };
- public static readonly GUIContent useNewHierarchy = EditorGUIUtility.TrTextContent("Use new Hierarchy window");
public static readonly GUIContent hierarchyHeader = EditorGUIUtility.TrTextContent("Hierarchy window");
- public static readonly GUIContent newHierarchyHeader = EditorGUIUtility.TrTextContent("New Hierarchy");
public static readonly GUIContent renameNewObjects = EditorGUIUtility.TrTextContent("Rename new objects");
public static readonly GUIContent defaultPrefabMode = EditorGUIUtility.TrTextContent("Default Prefab Mode", "This mode will be used when opening Prefab Mode from a Prefab instance in the Hierarchy.");
public static readonly GUIContent enableAlphaNumericSorting = EditorGUIUtility.TrTextContent("Enable Alphanumeric Sorting", "If enabled then you can choose between Transform sorting and Alphabetical sorting in the Hierarchy.");
@@ -86,8 +85,8 @@ class GeneralProperties
public static readonly GUIContent gameObjectIconMode = EditorGUIUtility.TrTextContent("GameObject Icons", "Controls how the new Hierarchy window replaces GameObject icons.");
public static readonly GUIContent[] gameObjectIconModeOptions =
{
- EditorGUIUtility.TrTextContent("Use Components and Gizmos"),
- EditorGUIUtility.TrTextContent("Use Components only"),
+ EditorGUIUtility.TrTextContent("Use components and custom icons"),
+ EditorGUIUtility.TrTextContent("Use components only"),
EditorGUIUtility.TrTextContent("Do not change GameObject icons"),
};
public static readonly GUIContent applicationFrameThrottling = EditorGUIUtility.TrTextContent("Frame Throttling (milliseconds)", "The number of milliseconds the Editor can idle between frames.");
@@ -115,6 +114,8 @@ class GeneralProperties
+ "\n* Debug: high-level debugging messages."
+ "\n* Silly: detailed debugging messages.");
public static readonly GUIContent logging = EditorGUIUtility.TrTextContent("Logging");
+ public static readonly GUIContent enableLoggingFramework = EditorGUIUtility.TrTextContent("Enable logging framework", "Enable the new logging framework (introduced in Unity 6.6) for high-performance logging with enhanced customisation options. Requires Editor restart to take effect.");
+ public static readonly GUIContent enableJSONLogging = EditorGUIUtility.TrTextContent("Enable JSON logging", "Output JSON logs to Editor.jsonl in addition to the standard Editor.log. Requires the logging framework to be enabled. Requires Editor restart to take effect.");
public static readonly GUIContent enableExtendedLogging = EditorGUIUtility.TrTextContent("Timestamp Editor log entries", "Adds timestamp and thread Id to Editor.log messages.");
public static readonly GUIContent useGlobalEditorLog = EditorGUIUtility.TrTextContent("Use Global Editor Log", "If enabled, all Unity projects use the same global Editor log file located at the default path for the operating system. If disabled, each project uses its own separate Editor log located in the project folder. Changes to this setting require an Editor restart to take effect.");
public static readonly GUIContent enableShortcutHelperBar = EditorGUIUtility.TrTextContent("Enable Shortcut Helper Bar", "Enables the Shortcut Helper Bar in the status bar at the bottom of the main Unity Editor window.");
@@ -146,6 +147,7 @@ class UIScalingProperties
class ColorsProperties
{
public static readonly GUIContent userDefaults = EditorGUIUtility.TrTextContent("Use Defaults");
+ public static readonly string lazyLoadingInfo = L10n.Tr("Some colors may not be available until you open their associated tool or window at least once.");
}
class GICacheProperties
@@ -212,6 +214,8 @@ class DeveloperModeProperties
private int[] m_CustomScalingValues = { 100, 125, 150, 175, 200, 225, 250, 300, 350 };
private bool m_EnableExtendedLogging;
private SavedBool m_UseGlobalEditorLog = new SavedBool("UseGlobalEditorLog", false);
+ private bool m_EnableLoggingFramework;
+ private bool m_EnableJSONLogging;
private readonly string kContentScalePrefKey = "CustomEditorUIScale";
private readonly string kWindowsTaskbarPrefKey = "WindowsTaskbarBehavior";
@@ -270,6 +274,7 @@ private struct GICacheSettings
private const int kRecentAppsCount = 10;
SortedDictionary>> s_CachedColors = null;
+ Dictionary s_ColorCategoryFoldouts = new Dictionary();
private List m_SystemFonts = new List();
private const int k_browseButtonWidth = 80;
@@ -689,23 +694,21 @@ private void ShowGeneral(string searchContext)
EditorGUI.indentLevel++;
HierarchyPreferences.DefaultPrefabModeFromHierarchy = (PrefabStage.Mode)EditorGUILayout.EnumPopup(GeneralProperties.defaultPrefabMode, HierarchyPreferences.DefaultPrefabModeFromHierarchy);
HierarchyPreferences.RenameNewObjects.value = EditorGUILayout.Toggle(GeneralProperties.renameNewObjects, HierarchyPreferences.RenameNewObjects);
- EditorGUI.BeginDisabled(HierarchyPreferences.UseNewHierarchy);
+ EditorGUI.BeginDisabled(!EditorSettings.useLegacyHierarchy);
bool oldAlphaNumeric = m_AllowAlphaNumericHierarchy;
m_AllowAlphaNumericHierarchy = EditorGUILayout.Toggle(GeneralProperties.enableAlphaNumericSorting, m_AllowAlphaNumericHierarchy);
EditorGUI.EndDisabled();
- GUILayout.Label(GeneralProperties.newHierarchyHeader);
- HierarchyPreferences.UseNewHierarchy.value = EditorGUILayout.Toggle(GeneralProperties.useNewHierarchy, HierarchyPreferences.UseNewHierarchy);
- EditorGUI.BeginDisabled(!HierarchyPreferences.UseNewHierarchy);
+ EditorGUI.BeginDisabled(EditorSettings.useLegacyHierarchy);
EditorGUI.BeginChangeCheck();
var alternatingRows = EditorGUILayout.Toggle(GeneralProperties.alternatingRowBackground, HierarchyPreferences.AlternatingRowBackground);
var useQueryBuilder = EditorGUILayout.Toggle(GeneralProperties.queryBuilder, HierarchyPreferences.UseQueryBuilder);
- var gameObjectIconMode = (HierarchyPreferences.IconMode)EditorGUILayout.Popup(GeneralProperties.gameObjectIconMode, (int)HierarchyPreferences.GameObjectIconMode, GeneralProperties.gameObjectIconModeOptions);
+ var gameObjectIconMode = EditorGUILayout.Popup(GeneralProperties.gameObjectIconMode, HierarchyPreferences.GameObjectIconMode.value, GeneralProperties.gameObjectIconModeOptions);
if (EditorGUI.EndChangeCheck())
{
HierarchyPreferences.AlternatingRowBackground.value = alternatingRows;
HierarchyPreferences.UseQueryBuilder.value = useQueryBuilder;
- HierarchyPreferences.GameObjectIconMode = gameObjectIconMode;
+ HierarchyPreferences.GameObjectIconMode.value = gameObjectIconMode;
}
EditorGUI.EndDisabled();
EditorGUI.indentLevel--;
@@ -816,8 +819,26 @@ private void DrawLoggingOptions()
GUILayout.Space(10);
EditorGUI.indentLevel++;
GUILayout.Label(GeneralProperties.logging, EditorStyles.boldLabel);
+
+ var prevLoggingFramework = m_EnableLoggingFramework;
+ var prevJsonLogging = m_EnableJSONLogging;
+
+ m_EnableLoggingFramework = EditorGUILayout.Toggle(GeneralProperties.enableLoggingFramework, m_EnableLoggingFramework);
+
+ using (new EditorGUI.DisabledScope(!m_EnableLoggingFramework))
+ {
+ m_EnableJSONLogging = EditorGUILayout.Toggle(GeneralProperties.enableJSONLogging, m_EnableJSONLogging);
+ }
+
+ LoggingSettingsAnalytics.SendChangedLoggingPreferences(prevLoggingFramework, m_EnableLoggingFramework, prevJsonLogging, m_EnableJSONLogging);
+
m_EnableExtendedLogging = EditorGUILayout.Toggle(GeneralProperties.enableExtendedLogging, m_EnableExtendedLogging);
- m_UseGlobalEditorLog.value = EditorGUILayout.Toggle(GeneralProperties.useGlobalEditorLog, m_UseGlobalEditorLog.value);
+
+ using (new EditorGUI.DisabledScope(m_EnableLoggingFramework))
+ {
+ m_UseGlobalEditorLog.value = EditorGUILayout.Toggle(GeneralProperties.useGlobalEditorLog, m_UseGlobalEditorLog.value);
+ }
+
EditorGUI.indentLevel--;
}
@@ -918,13 +939,15 @@ private void RevertColors()
private void ShowColors(string searchContext)
{
+ EditorGUILayout.HelpBox(ColorsProperties.lazyLoadingInfo, MessageType.Info);
+ EditorGUILayout.Space();
+
if (s_CachedColors == null)
{
s_CachedColors = OrderPrefs(PrefSettings.Prefs());
}
var changedColor = false;
- PrefColor ccolor = null;
// some pref colors are very long, and changing them would mean invalidating any user-defined colors.
// as a compromise, we'll clip the label with an ellipses and show the full text in a tooltip.
@@ -933,21 +956,28 @@ private void ShowColors(string searchContext)
foreach (KeyValuePair>> category in s_CachedColors)
{
- GUILayout.Label(category.Key, EditorStyles.boldLabel);
- foreach (KeyValuePair kvp in category.Value)
+ // Default to collapsed (false) if not yet in dictionary
+ s_ColorCategoryFoldouts.TryGetValue(category.Key, out bool expanded);
+ expanded = EditorGUILayout.Foldout(expanded, category.Key, true);
+ s_ColorCategoryFoldouts[category.Key] = expanded;
+
+ if (expanded)
{
- var displayName = ObjectNames.NicifyVariableName(kvp.Key);
- EditorGUI.BeginChangeCheck();
- Color c = EditorGUILayout.ColorField(EditorGUIUtility.TempContent(displayName, $"Custom overlay color for windows of {displayName} type"), kvp.Value.Color);
- if (EditorGUI.EndChangeCheck())
+ EditorGUI.indentLevel++;
+ foreach (KeyValuePair kvp in category.Value)
{
- ccolor = kvp.Value;
- ccolor.Color = c;
- changedColor = true;
+ var displayName = ObjectNames.NicifyVariableName(kvp.Key);
+ EditorGUI.BeginChangeCheck();
+ Color c = EditorGUILayout.ColorField(EditorGUIUtility.TempContent(displayName, $"Custom overlay color for windows of {displayName} type"), kvp.Value.Color);
+ if (EditorGUI.EndChangeCheck())
+ {
+ kvp.Value.Color = c;
+ PrefSettings.Set(kvp.Value.Name, kvp.Value);
+ changedColor = true;
+ }
}
+ EditorGUI.indentLevel--;
}
- if (ccolor != null)
- PrefSettings.Set(ccolor.Name, ccolor);
}
EditorStyles.label.clipping = clipping;
@@ -1291,7 +1321,7 @@ private void WritePreferences()
EditorTextSettings.SetEditorTextGeneratorType(m_EditorTextGeneratorType);
EditorApplication.RequestRepaintAllTexts(VersionChangeType.Repaint);
- EditorPrefs.SetBool("AllowAlphaNumericHierarchy", m_AllowAlphaNumericHierarchy);
+ HierarchyPreferences.AllowAlphaNumericHierarchy.value = m_AllowAlphaNumericHierarchy;
EditorPrefs.SetFloat("EditorBusyProgressDialogDelay", m_ProgressDialogDelay);
GOCreationCommands.s_PlacementMode = m_CreatePlacementMode;
@@ -1314,6 +1344,8 @@ private void WritePreferences()
EditorPrefs.SetBool("EnableConstrainProportionsTransformScale", m_EnableConstrainProportionsScalingForNewObjects);
EditorPrefs.SetBool("UseInspectorExpandedState", AnnotationUtility.useInspectorExpandedState);
EditorPrefs.SetBool("EnableExtendedLogging", m_EnableExtendedLogging);
+ EditorPrefs.SetBool("EnableLoggingFramework", m_EnableLoggingFramework);
+ EditorPrefs.SetBool("EnableJSONLogging", m_EnableJSONLogging);
}
private int CurrentEditorScalingValue
@@ -1388,7 +1420,7 @@ private void ReadPreferences()
m_EditorTextGeneratorType = (TextGeneratorType)EditorPrefs.GetInt("EditorTextGeneratorTypeV2", (int)TextGeneratorType.Advanced);
EditorTextSettings.SetCurrentEditorSharpness(m_EditorTextSharpness);
- m_AllowAlphaNumericHierarchy = EditorPrefs.GetBool("AllowAlphaNumericHierarchy", false);
+ m_AllowAlphaNumericHierarchy = HierarchyPreferences.AllowAlphaNumericHierarchy;
m_ProgressDialogDelay = EditorPrefs.GetFloat("EditorBusyProgressDialogDelay", 3.0f);
m_CreatePlacementMode = GOCreationCommands.s_PlacementMode;
@@ -1428,6 +1460,8 @@ private void ReadPreferences()
m_GraphSnapping = EditorPrefs.GetBool("GraphSnapping", true);
m_EnableExtendedLogging = EditorPrefs.GetBool("EnableExtendedLogging", false);
+ m_EnableLoggingFramework = EditorPrefs.GetBool("EnableLoggingFramework", LoggingSettingsAnalytics.DefaultLoggingFrameworkEnabled);
+ m_EnableJSONLogging = EditorPrefs.GetBool("EnableJSONLogging", false);
}
internal static void ReloadCustomDiffToolData()
@@ -1600,4 +1634,133 @@ private string[] BuildFriendlyAppNameList(string[] appPathList, Dictionary HierarchyPreferences.DefaultPrefabModeFromHierarchy;
}
+
+ internal interface ILoggingSettingsAnalyticsService
+ {
+ AnalyticsResult SendAnalytic(IAnalytic analytic);
+ }
+
+ internal class LoggingSettingsEditorAnalyticsService : ILoggingSettingsAnalyticsService
+ {
+ AnalyticsResult ILoggingSettingsAnalyticsService.SendAnalytic(IAnalytic analytic)
+ {
+ return EditorAnalytics.SendAnalytic(analytic);
+ }
+ }
+
+ internal static class LoggingSettingsAnalytics
+ {
+ const string k_LoggingSettingEventName = "logging_setting_changed";
+ const string k_LoggingJsonEventName = "logging_json_changed";
+ const int k_MaxEventsPerHour = 100;
+ const string k_VendorKey = "unity.logging";
+ internal const bool DefaultLoggingFrameworkEnabled = true;
+ static Action s_TestEventCallback;
+
+ [Serializable]
+ internal struct LoggingSettingChangedData : IAnalytic.IData
+ {
+ public bool enabled;
+ public bool uses_default;
+ }
+
+ [Serializable]
+ internal struct LoggingJsonChangedData : IAnalytic.IData
+ {
+ public bool enabled;
+ }
+
+ [AnalyticInfo(eventName: k_LoggingSettingEventName, vendorKey: k_VendorKey, version: 1, maxEventsPerHour: k_MaxEventsPerHour)]
+ internal class LoggingSettingChangedAnalytic : IAnalytic
+ {
+ readonly LoggingSettingChangedData m_Data;
+
+ public LoggingSettingChangedAnalytic(LoggingSettingChangedData data)
+ {
+ m_Data = data;
+ }
+
+ public bool TryGatherData(out IAnalytic.IData data, out Exception error)
+ {
+ data = m_Data;
+ error = null;
+ return true;
+ }
+ }
+
+ [AnalyticInfo(eventName: k_LoggingJsonEventName, vendorKey: k_VendorKey, version: 1, maxEventsPerHour: k_MaxEventsPerHour)]
+ internal class LoggingJsonChangedAnalytic : IAnalytic
+ {
+ readonly LoggingJsonChangedData m_Data;
+
+ public LoggingJsonChangedAnalytic(LoggingJsonChangedData data)
+ {
+ m_Data = data;
+ }
+
+ public bool TryGatherData(out IAnalytic.IData data, out Exception error)
+ {
+ data = m_Data;
+ error = null;
+ return true;
+ }
+ }
+
+ static ILoggingSettingsAnalyticsService s_AnalyticsService;
+
+ static LoggingSettingsAnalytics()
+ {
+ if (!InternalEditorUtility.inBatchMode)
+ SetAnalyticsService(new LoggingSettingsEditorAnalyticsService());
+ }
+
+ public static ILoggingSettingsAnalyticsService SetAnalyticsService(ILoggingSettingsAnalyticsService service)
+ {
+ var oldService = s_AnalyticsService;
+ s_AnalyticsService = service;
+ return oldService;
+ }
+
+ internal static Action SetTestEventCallback(Action callback)
+ {
+ var oldCallback = s_TestEventCallback;
+ s_TestEventCallback = callback;
+ return oldCallback;
+ }
+
+ internal static void SendChangedLoggingPreferences(bool prevFramework, bool currFramework, bool prevJson, bool currJson)
+ {
+ if (prevFramework != currFramework)
+ SendLoggingSettingChanged(currFramework, currFramework == DefaultLoggingFrameworkEnabled);
+ if (prevJson != currJson)
+ SendLoggingJsonChanged(currJson);
+ }
+
+ public static void SendLoggingSettingChanged(bool enabled, bool usesDefault)
+ {
+ s_TestEventCallback?.Invoke(k_LoggingSettingEventName, enabled, usesDefault);
+
+ if (s_AnalyticsService == null)
+ return;
+
+ s_AnalyticsService.SendAnalytic(new LoggingSettingChangedAnalytic(new LoggingSettingChangedData
+ {
+ enabled = enabled,
+ uses_default = usesDefault,
+ }));
+ }
+
+ public static void SendLoggingJsonChanged(bool enabled)
+ {
+ s_TestEventCallback?.Invoke(k_LoggingJsonEventName, enabled, false);
+
+ if (s_AnalyticsService == null)
+ return;
+
+ s_AnalyticsService.SendAnalytic(new LoggingJsonChangedAnalytic(new LoggingJsonChangedData
+ {
+ enabled = enabled,
+ }));
+ }
+ }
}
diff --git a/Editor/Mono/ProjectBrowser/ProjectBrowser.cs b/Editor/Mono/ProjectBrowser/ProjectBrowser.cs
index 107236c65f..ded87cb7d0 100644
--- a/Editor/Mono/ProjectBrowser/ProjectBrowser.cs
+++ b/Editor/Mono/ProjectBrowser/ProjectBrowser.cs
@@ -3173,24 +3173,25 @@ private void FrameObjectPrivate(EntityId entityId, bool frame, bool ping)
private void FrameObjectInTwoColumnMode(EntityId entityId, bool frame, bool ping)
{
EntityId folderEntityId = EntityId.None;
+ string assetPath = AssetDatabase.GetAssetPath(entityId);
- if (entityId == kPackagesFolderInstanceId)
+ // Root folders for Assets and Packages are special, as we cannot show their parent folder
+ // we simply ping them in the folder tree and show their contents
+ if (assetPath == "Assets")
+ folderEntityId = entityId;
+ else if (entityId == kPackagesFolderInstanceId)
folderEntityId = kPackagesFolderInstanceId;
else
{
- string assetPath = AssetDatabase.GetAssetPath((EntityId)entityId);
if (!String.IsNullOrEmpty(assetPath))
{
string containingFolder = ProjectWindowUtil.GetContainingFolder(assetPath);
if (!String.IsNullOrEmpty(containingFolder))
folderEntityId = GetFolderInstanceID(containingFolder);
-
- if (folderEntityId == EntityId.None)
- folderEntityId = AssetDatabase.GetMainAssetOrInProgressProxyEntityId("Assets");
}
}
- // Could be a scene gameobject
+ // folderEntityId stays None for a scene GameObject or an out-of-tree asset (e.g. ProjectSettings); leave navigation unchanged.
if (folderEntityId != EntityId.None)
{
m_FolderTree.Frame(folderEntityId, frame, ping);
diff --git a/Editor/Mono/ProjectBrowser/SearchFilter.cs b/Editor/Mono/ProjectBrowser/SearchFilter.cs
index e0e69e57cf..99393fd0e1 100644
--- a/Editor/Mono/ProjectBrowser/SearchFilter.cs
+++ b/Editor/Mono/ProjectBrowser/SearchFilter.cs
@@ -14,7 +14,7 @@
namespace UnityEditor
{
[Serializable]
- [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.ShaderFoundryModule")]
+ [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.ShaderFoundryModule", "UnityEditor.UIToolkitAuthoringModule")]
[DataContract]
internal class SearchFilter
{
diff --git a/Editor/Mono/SceneHierarchyWindow.cs b/Editor/Mono/SceneHierarchyWindow.cs
index 54d276c33d..f688ff0ab4 100644
--- a/Editor/Mono/SceneHierarchyWindow.cs
+++ b/Editor/Mono/SceneHierarchyWindow.cs
@@ -69,7 +69,7 @@ public override void OnEnable()
PrefabUtility.prefabInstanceModificationCacheCleared += OnPrefabInstanceModificationCacheCleared;
- HierarchyPreferences.UseNewHierarchy.valueChanged += OnUseNewHierarchyChanged;
+ EditorSettings.useLegacyHierarchyChanged += OnUseLegacyHierarchyChanged;
}
private void OnPrefabInstanceModificationCacheCleared()
@@ -89,10 +89,10 @@ public override void OnDisable()
m_StageHandling.OnDisable();
PrefabUtility.prefabInstanceModificationCacheCleared -= OnPrefabInstanceModificationCacheCleared;
- HierarchyPreferences.UseNewHierarchy.valueChanged -= OnUseNewHierarchyChanged;
+ EditorSettings.useLegacyHierarchyChanged -= OnUseLegacyHierarchyChanged;
}
- void OnUseNewHierarchyChanged() => HierarchyPreferences.EnsureCorrectHierarchyIsInUse(this);
+ void OnUseLegacyHierarchyChanged() => HierarchyPreferences.EnsureCorrectHierarchyIsInUse(this);
internal override void ClickedSearchField()
{
diff --git a/Editor/Mono/SceneModeWindows/DefaultLightingExplorerExtension.cs b/Editor/Mono/SceneModeWindows/DefaultLightingExplorerExtension.cs
index 9053353f65..7bc4b0b234 100644
--- a/Editor/Mono/SceneModeWindows/DefaultLightingExplorerExtension.cs
+++ b/Editor/Mono/SceneModeWindows/DefaultLightingExplorerExtension.cs
@@ -228,7 +228,7 @@ protected virtual LightingExplorerTableColumn[] Get2DLightColumns()
}
}
}, null, null, new[] { 2 }), // 7: Falloff intensity
- new LightingExplorerTableColumn(LightingExplorerTableColumn.DataType.Name, Styles.TargetSortingLayer, "m_ApplyToSortingLayers", 120, // 8
+ new LightingExplorerTableColumn(LightingExplorerTableColumn.DataType.Custom, Styles.TargetSortingLayer, "m_ApplyToSortingLayers", 120, // 8
(r, prop, dep) =>
{
if (prop != null && prop.isArray)
diff --git a/Editor/Mono/SceneModeWindows/LightingWindow.cs b/Editor/Mono/SceneModeWindows/LightingWindow.cs
index 166c128227..c94342ff2f 100644
--- a/Editor/Mono/SceneModeWindows/LightingWindow.cs
+++ b/Editor/Mono/SceneModeWindows/LightingWindow.cs
@@ -50,8 +50,8 @@ static class Styles
public static readonly GUIContent unsupportedDenoisersLabel = EditorGUIUtility.TrTextContentWithIcon("Unsupported denoiser selected", MessageType.Error);
public static readonly GUIContent cannotBakeRosettaNotInstalledLabel = EditorGUIUtility.TrTextContentWithIcon("Unable to start the baking process as the required version of Apple Rosetta could not be found", MessageType.Error);
- public static readonly GUIContent GPUUseHardwareRayTracing = EditorGUIUtility.TrTextContent("Hardware ray tracing", "Use hardware ray tracing if the GPU device supports it.");
- public static readonly GUIContent GPUUseHardwareRayTracingNotSupported = EditorGUIUtility.TrTextContent("Hardware ray tracing", "Hardware ray tracing is not supported by the GPU device.");
+ public static readonly GUIContent GPUUseHardwareRayTracing = EditorGUIUtility.TrTextContent("Hardware Ray Tracing", "Use hardware ray tracing if the GPU device supports it.");
+ public static readonly GUIContent GPUUseHardwareRayTracingNotSupported = EditorGUIUtility.TrTextContent("Hardware Ray Tracing", "Hardware ray tracing is not supported by the GPU device.");
public static readonly int[] progressiveGPUUnknownDeviceValues = { 0 };
public static readonly GUIContent[] progressiveGPUUnknownDeviceStrings =
@@ -536,7 +536,9 @@ void DrawGPUDeviceSelector()
void DrawBakingProfileSelector()
{
// Handle the baking profile setting
+#pragma warning disable 618 // ProgressiveCPU is deprecated; comparison disables the GPU baking profile when CPU is selected.
using (new EditorGUI.DisabledScope(Lightmapping.GetLightingSettingsOrDefaultsFallback().lightmapper == LightingSettings.Lightmapper.ProgressiveCPU))
+#pragma warning restore 618
{
int bakingProfile = Styles.bakingProfileDefault;
string bakingProfileString = EditorUserSettings.GetConfigValue(m_BakingProfileKey);
diff --git a/Editor/Mono/SceneView/RectSelection.cs b/Editor/Mono/SceneView/RectSelection.cs
index 8d394f9939..5c922087eb 100644
--- a/Editor/Mono/SceneView/RectSelection.cs
+++ b/Editor/Mono/SceneView/RectSelection.cs
@@ -182,7 +182,8 @@ void Pick(SelectionType selectionType, Vector2 mousePos, Event evt)
if (!handledIt)
{
var picked = HandleUtility.PickObject(mousePos, true);
- UpdateSelection(m_SelectionStart, hoveredObj, SelectionType.Additive, false);
+ var pickedObj = Object.FindObjectFromInstanceID(picked.targetId);
+ UpdateSelection(m_SelectionStart, pickedObj, SelectionType.Additive, false);
}
}
else // With no modifier keys, we do the "cycle through overlapped" picking logic in SceneViewPicking.cs
diff --git a/Editor/Mono/SceneView/SceneView.cs b/Editor/Mono/SceneView/SceneView.cs
index 3aae377063..535b6c753f 100644
--- a/Editor/Mono/SceneView/SceneView.cs
+++ b/Editor/Mono/SceneView/SceneView.cs
@@ -37,7 +37,7 @@ namespace UnityEditor
[EditorWindowTitle(title = "Scene", useTypeNameAsIconName = true)]
[NativeHeader("Editor/Src/SceneView/SceneViewBindings.h")]
[EditorToolOwner(typeof(GameObjectToolContext))]
- public partial class SceneView : SearchableEditorWindow, IHasCustomMenu, ISupportsOverlays, ISupportsToolsOverlays
+ public partial class SceneView : SearchableEditorWindow, IHasCustomMenu, ISupportsEditorTools
{
[Serializable]
public struct CameraMode
@@ -659,7 +659,7 @@ internal bool debugDrawModesUseInteractiveLightBakingData
}
internal bool usesInteractiveLightBakingData => this.debugDrawModesUseInteractiveLightBakingData && this.currentDrawModeMayUseInteractiveLightBakingData;
-
+
[SerializeField]
// used by Tests/EditModeAndPlayModeTests/SceneView/CameraFlyModeContextTests
internal AnimVector3 m_Position = new AnimVector3(kDefaultPivot);
@@ -1049,7 +1049,7 @@ public interface IAdditionalSettings : ICloneable
void Apply(); // Called from copy-past logic (SceneViewCameraWindow)
internal Type filteringRenderPipelineAssetType { get; }
}
-
+
// Template class is implementing basic functionalities instead of doing all in final class
[Serializable]
public abstract class AdditionalSettings : IAdditionalSettings
@@ -1074,7 +1074,7 @@ Type IAdditionalSettings.filteringRenderPipelineAssetType
[SerializeReference]
List m_AdditionalSettings = new();
-
+
int FindAdditionalSettingsIndexForRenderPipelineType(Type rpType)
{
for (int i = m_AdditionalSettings.Count-1; i >= 0; --i)
@@ -1342,6 +1342,8 @@ static Styles()
public Camera camera { get { return m_Camera; } }
+ Camera ISupportsEditorTools.handlesCamera => m_Camera;
+
[SerializeField]
Shader m_ReplacementShader;
[SerializeField]
diff --git a/Editor/Mono/SceneView/SceneViewToolbars.cs b/Editor/Mono/SceneView/SceneViewToolbars.cs
index a2b366d4ea..85008ee821 100644
--- a/Editor/Mono/SceneView/SceneViewToolbars.cs
+++ b/Editor/Mono/SceneView/SceneViewToolbars.cs
@@ -16,7 +16,7 @@ namespace UnityEditor
// - UIServiceEditor/SceneView/SceneViewToolbarElements.cs
// - UIServiceEditor/EditorToolbar/ToolbarElements/BuiltinTools.cs
- [Overlay(typeof(ISupportsToolsOverlays), k_Id, "Tools", true, priority = (int)OverlayPriority.Tools, defaultDockZone = DockZone.LeftColumn, defaultDockPosition = DockPosition.Top, defaultLayout = Layout.VerticalToolbar, defaultDockIndex = 0, group = OverlayAttribute.unityGroup)]
+ [Overlay(typeof(ISupportsEditorTools), k_Id, "Tools", true, priority = (int)OverlayPriority.Tools, defaultDockZone = DockZone.LeftColumn, defaultDockPosition = DockPosition.Top, defaultLayout = Layout.VerticalToolbar, defaultDockIndex = 0, group = OverlayAttribute.unityGroup)]
[Icon("Icons/Overlays/ToolsToggle.png")]
class TransformToolsOverlayToolBar : ToolbarOverlay
{
diff --git a/Editor/Mono/SceneVisibility/SceneVisibilityManager.cs b/Editor/Mono/SceneVisibility/SceneVisibilityManager.cs
index e9f55d2784..825aa72d4a 100644
--- a/Editor/Mono/SceneVisibility/SceneVisibilityManager.cs
+++ b/Editor/Mono/SceneVisibility/SceneVisibilityManager.cs
@@ -708,7 +708,7 @@ private static void ToggleSelectionPicking(bool includeChildren)
instance.PickableContentChanged();
- if (!HierarchyPreferences.UseNewHierarchy)
+ if (EditorSettings.useLegacyHierarchy)
EditorApplication.RepaintHierarchyWindow();
}
diff --git a/Editor/Mono/Scripting/EditModeScope.cs b/Editor/Mono/Scripting/EditModeScope.cs
index 3c5fdfb168..ec37b32f42 100644
--- a/Editor/Mono/Scripting/EditModeScope.cs
+++ b/Editor/Mono/Scripting/EditModeScope.cs
@@ -34,13 +34,15 @@ protected override void Enter(ScopeTransitionHelper scopeTransitionHelper)
return;
}
- DebugLifecycle.Log($"Lifecycle : Entering {ScopeName} scope");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Entering {ScopeName} scope");
scopeTransitionHelper.ExecuteMethodsInOrder();
}
protected override void Exit(ScopeTransitionHelper scopeTransitionHelper)
{
- DebugLifecycle.Log($"Lifecycle : Exiting {ScopeName} scope");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Exiting {ScopeName} scope");
scopeTransitionHelper.ExecuteMethodsInReverseOrder();
}
}
diff --git a/Editor/Mono/Scripting/EditModeScopePostprocessor.cs b/Editor/Mono/Scripting/EditModeScopePostprocessor.cs
new file mode 100644
index 0000000000..15ecb451ea
--- /dev/null
+++ b/Editor/Mono/Scripting/EditModeScopePostprocessor.cs
@@ -0,0 +1,26 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using Unity.Scripting.LifecycleManagement;
+using UnityEditor;
+using UnityEditor.Scripting.LifecycleManagement;
+using UnityEngine;
+
+// This class is responsible for Entering EditModeScope after a domain reload
+// after all assets has been processed
+sealed class EditModeScopePostprocessor : AssetPostprocessor
+{
+ static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload)
+ {
+ // Guard against re-entering EditModeScope when it is already active.
+ // When script compilation fails, the domain reload is skipped (assemblies are not reloaded)
+ // but didDomainReload can still be true from a prior reload in the same import cycle.
+ // In that case EditModeScope was never exited and must not be entered again.
+ if (didDomainReload && !EditorApplication.isPlayingOrWillChangePlaymode
+ && !LifecycleController.Instance.IsScopePresent())
+ {
+ LifecycleController.Instance.EnterScope();
+ }
+ }
+}
diff --git a/Editor/Mono/Scripting/ScriptCompilation/AssetPathMetaData.cs b/Editor/Mono/Scripting/ScriptCompilation/AssetPathMetaData.cs
index 7a31f82731..c4d372bd03 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/AssetPathMetaData.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/AssetPathMetaData.cs
@@ -13,8 +13,8 @@ namespace UnityEditor.Scripting.ScriptCompilation
[NativeAsStruct]
[StructLayout(LayoutKind.Sequential)]
[RequiredByNativeCode(GenerateProxy = true)]
- [NativeHeader("Runtime/Scripting/ScriptingManagedProxySupport.h")]
- [NativeHeader("Runtime/ScriptingBackend/ScriptingNativeTypes.h")]
+ [NativeHeader("Scripting/ScriptingManagedProxySupport.h")]
+ [NativeHeader("Scripting/ScriptingBackend/ScriptingNativeTypes.h")]
[DebuggerDisplay("{DirectoryPath}")]
class AssetPathMetaData
{
diff --git a/Editor/Mono/Scripting/ScriptCompilation/AssetPathVersionMetaData.cs b/Editor/Mono/Scripting/ScriptCompilation/AssetPathVersionMetaData.cs
index 24f0299c58..cf7335a9f6 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/AssetPathVersionMetaData.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/AssetPathVersionMetaData.cs
@@ -20,7 +20,7 @@ enum VersionType
[NativeAsStruct]
[StructLayout(LayoutKind.Sequential)]
[RequiredByNativeCode(GenerateProxy = true)]
- [NativeHeader("Runtime/Scripting/ScriptingManagedProxySupport.h")]
+ [NativeHeader("Scripting/ScriptingManagedProxySupport.h")]
class VersionMetaData
{
public string Name;
diff --git a/Editor/Mono/Scripting/ScriptCompilation/BeeDriver/UnityBeeDriverProfilerSession.cs b/Editor/Mono/Scripting/ScriptCompilation/BeeDriver/UnityBeeDriverProfilerSession.cs
index 9d0bbfed86..0a1f5576cc 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/BeeDriver/UnityBeeDriverProfilerSession.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/BeeDriver/UnityBeeDriverProfilerSession.cs
@@ -33,17 +33,22 @@ static public void Finish()
if (m_CurrentPlayerBuildProfilerOutputFile == null)
return;
+ // Clear the session before writing, so a failed write still leaves it finished. Otherwise the
+ // next Finish() retries the write and reports the same failure again.
+ var outputFile = m_CurrentPlayerBuildProfilerOutputFile;
+ var tinyProfiler = _tinyProfiler;
+ m_CurrentPlayerBuildProfilerOutputFile = null;
+ _tinyProfiler = null;
+
foreach (var task in m_TasksToWaitForBeforeFinishing)
task.Wait();
- _tinyProfiler.Write(m_CurrentPlayerBuildProfilerOutputFile.ToString(), new ChromeTraceOptions
+ tinyProfiler.Write(outputFile.ToString(), new ChromeTraceOptions
{
ProcessName = "Unity",
ProcessId = System.Diagnostics.Process.GetCurrentProcess().Id,
ProcessSortIndex = -100
});
- m_CurrentPlayerBuildProfilerOutputFile = null;
- _tinyProfiler = null;
}
static public void BeginSection(string name)
diff --git a/Editor/Mono/Scripting/ScriptCompilation/EditorBuildRules.cs b/Editor/Mono/Scripting/ScriptCompilation/EditorBuildRules.cs
index 2196e1f00f..66d593a4ae 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/EditorBuildRules.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/EditorBuildRules.cs
@@ -91,7 +91,7 @@ public bool Contains(string define)
public static Dictionary predefinedTargetAssemblies { get; private set; }
- private static readonly string[] s_CSharpVersionDefines =
+ internal static readonly string[] s_CSharpVersionDefines =
{
"CSHARP_7_OR_LATER", // Incremental Compiler adds this.
"CSHARP_7_3_OR_NEWER",
diff --git a/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs b/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs
index 40f5e9092a..01e2f9e581 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs
@@ -220,11 +220,7 @@ public static bool IsCompilationInProgress()
[RequiredByNativeCode]
public static void IsCompiling(out bool isCompiling)
{
- if (MsBuildCompilationInterface.IsEnabled())
- {
- isCompiling = MsBuildCompilationInterface.Instance.IsCompiling();
- }
- isCompiling = Instance.IsCompiling();
+ isCompiling = MsBuildCompilationInterface.IsEnabled() ? MsBuildCompilationInterface.Instance.IsCompiling() : Instance.IsCompiling();
}
[RequiredByNativeCode]
diff --git a/Editor/Mono/Scripting/ScriptCompilation/MsBuild/MsBuildCompilation.cs b/Editor/Mono/Scripting/ScriptCompilation/MsBuild/MsBuildCompilation.cs
index 9d1bb81dec..1f06a2bf97 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/MsBuild/MsBuildCompilation.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/MsBuild/MsBuildCompilation.cs
@@ -309,7 +309,8 @@ public void SetAllScripts(string[] allScripts)
precompiledAssemblyPaths = precompiledPaths,
precompiledAssemblyExplicitlyReferenced = precompiledExplicitlyReferenced,
errors = new List(),
- warnings = new List()
+ warnings = new List(),
+ predefinedAssembliesAllowUnsafeCode = PlayerSettings.allowUnsafeCode
};
AsmDefConverter.Convert(context);
diff --git a/Editor/Mono/Scripting/ScriptCompilation/MsBuild/UnityEditorMSBuildPropsTargetsGeneration.cs b/Editor/Mono/Scripting/ScriptCompilation/MsBuild/UnityEditorMSBuildPropsTargetsGeneration.cs
index 5687335897..f57fcc0880 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/MsBuild/UnityEditorMSBuildPropsTargetsGeneration.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/MsBuild/UnityEditorMSBuildPropsTargetsGeneration.cs
@@ -96,8 +96,8 @@ public static void UpdateGeneratedMSBuildFileIfNeeded(BuildTarget buildTarget, M
UpdateSystemSearchPaths(buildTarget);
UpdateRoslynAnalyzersProps();
- var optimization = CompilationPipeline.codeOptimization;
- PropsGenerator.Instance.UpdateUnityContentLocation(EditorApplication.applicationScriptingPath, buildTarget.ToString(), GetCurrentDotNETRuntimeId(), optimization == CodeOptimization.Release);
+ PropsGenerator.Instance.UpdateUnityContentLocation(EditorApplication.applicationScriptingPath);
+ PropsGenerator.Instance.UpdateBuildConfigurationProperties(buildTarget.ToString(), CompilationPipeline.codeOptimization == CodeOptimization.Release);
}
private static void UpdateRoslynAnalyzersProps()
@@ -122,9 +122,6 @@ public static void UpdateEssentialPropsOnly(BuildTarget buildTarget)
PropsGenerator.Instance.UpdateEssentialPropsOnly(
EditorApplication.applicationScriptingPath,
- buildTarget.ToString(),
- GetCurrentDotNETRuntimeId(),
- optimization == CodeOptimization.Release,
editorVersion);
}
@@ -145,11 +142,11 @@ public static void UpdateDeferrablePropsInParallel(BuildTarget buildTarget, MSBu
var editorApiCompatibility =
PlayerSettings.EditorAssemblyCompatibilityToApiCompatibility(PlayerSettings
.GetEditorAssembliesCompatibilityLevel());
- var editorOnlyCompatibleDefines = InternalEditorUtility.GetCompilationDefines(
- editorScriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility);
+ var editorOnlyCompatibleDefines = AppendCSharpVersionDefines(InternalEditorUtility.GetCompilationDefines(
+ editorScriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility));
- var playerAssembliesDefines = InternalEditorUtility.GetCompilationDefines(
- scriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility);
+ var playerAssembliesDefines = AppendCSharpVersionDefines(InternalEditorUtility.GetCompilationDefines(
+ scriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility));
// Get search paths
var searchPaths = BuildPlayerDataGenerator.GetStaticSearchPaths(buildTarget);
@@ -163,7 +160,9 @@ public static void UpdateDeferrablePropsInParallel(BuildTarget buildTarget, MSBu
cache.PlayerPluginPaths,
cache.AllPlugins,
searchPaths,
- cache.RoslynAnalyzerPaths);
+ cache.RoslynAnalyzerPaths,
+ buildTarget.ToString(),
+ CompilationPipeline.codeOptimization == CodeOptimization.Release);
}
private static string GetCurrentDotNETRuntimeId()
@@ -331,14 +330,26 @@ private static void UpdateDefinesProps(BuildTarget buildTarget, MSBuildCompilati
var editorApiCompatibility =
PlayerSettings.EditorAssemblyCompatibilityToApiCompatibility(PlayerSettings
.GetEditorAssembliesCompatibilityLevel());
- var editorOnlyCompatibleDefines = InternalEditorUtility.GetCompilationDefines(
- editorScriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility);
+ var editorOnlyCompatibleDefines = AppendCSharpVersionDefines(InternalEditorUtility.GetCompilationDefines(
+ editorScriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility));
- var playerAssembliesDefines = InternalEditorUtility.GetCompilationDefines(
- scriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility);
+ var playerAssembliesDefines = AppendCSharpVersionDefines(InternalEditorUtility.GetCompilationDefines(
+ scriptCompilationOptions, buildTarget, subtarget, editorApiCompatibility));
PropsGenerator.Instance.UpdateDefinesProps(editorOnlyCompatibleDefines, playerAssembliesDefines);
}
+ // Mirrors EditorBuildRules.GetScriptAssemblies, which appends the C# language-version
+ // defines to every assembly. The legacy pipeline adds these per-assembly; MSBU emits them
+ // into the shared DefineConstants props so all generated projects compile with them.
+ private static string[] AppendCSharpVersionDefines(string[] defines)
+ {
+ var versionDefines = EditorBuildRules.s_CSharpVersionDefines;
+ var result = new string[defines.Length + versionDefines.Length];
+ Array.Copy(defines, result, defines.Length);
+ Array.Copy(versionDefines, 0, result, defines.Length, versionDefines.Length);
+ return result;
+ }
+
private static EditorScriptCompilationOptions MapMSBuildCompilationOptions(
MSBuildCompilationOptions compilationOptions)
{
diff --git a/Editor/Mono/Scripting/ScriptCompilation/PrecompiledAssembly.cs b/Editor/Mono/Scripting/ScriptCompilation/PrecompiledAssembly.cs
index 43c7642863..f88f982e63 100644
--- a/Editor/Mono/Scripting/ScriptCompilation/PrecompiledAssembly.cs
+++ b/Editor/Mono/Scripting/ScriptCompilation/PrecompiledAssembly.cs
@@ -14,7 +14,7 @@ namespace UnityEditor.Scripting.ScriptCompilation
{
[DebuggerDisplay("{Path}")]
[NativeHeader("Editor/Src/ScriptCompilation/ScriptCompilationPipeline.h")]
- [NativeHeader("Runtime/Scripting/ScriptingTypes.h")]
+ [NativeHeader("Scripting/ScriptingTypes.h")]
[StructLayout(LayoutKind.Sequential)]
struct PrecompiledAssembly
{
diff --git a/Editor/Mono/Scripting/ScriptCompilers.cs b/Editor/Mono/Scripting/ScriptCompilers.cs
index 0f9dc7b141..2406deaa44 100644
--- a/Editor/Mono/Scripting/ScriptCompilers.cs
+++ b/Editor/Mono/Scripting/ScriptCompilers.cs
@@ -66,8 +66,7 @@ static extern bool CreateProcessW(
internal static void Cleanup()
{
- var isWindows = Application.platform == RuntimePlatform.WindowsEditor;
- if (isWindows)
+ if (Application.platform == RuntimePlatform.WindowsEditor)
{
// Use CreateProcessW as opposed to C# Process class to run
// the script so that we could disable handle inheritance
@@ -82,14 +81,17 @@ internal static void Cleanup()
}
else
{
- // Fire-and-forget: Don't wait for exit to avoid blocking the editor
- using var _ = System.Diagnostics.Process.Start(
- new System.Diagnostics.ProcessStartInfo(NetCoreProgram.DotNetMuxerPath.ToString())
- {
- Arguments = "build-server shutdown",
- UseShellExecute = false,
- CreateNoWindow = true
- });
+ var muxerPath = NetCoreProgram.DotNetMuxerPath.ToString();
+ System.Threading.ThreadPool.QueueUserWorkItem(_ =>
+ {
+ using var process = System.Diagnostics.Process.Start(
+ new System.Diagnostics.ProcessStartInfo(muxerPath)
+ {
+ Arguments = "build-server shutdown",
+ UseShellExecute = false,
+ CreateNoWindow = true
+ });
+ });
}
}
}
diff --git a/Editor/Mono/Serialization/DeserializationWarningHandler.cs b/Editor/Mono/Serialization/DeserializationWarningHandler.cs
new file mode 100644
index 0000000000..6483347cc0
--- /dev/null
+++ b/Editor/Mono/Serialization/DeserializationWarningHandler.cs
@@ -0,0 +1,81 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using System.Threading;
+using Unity.Scripting.LifecycleManagement;
+using UnityEngine;
+
+namespace UnityEditor
+{
+ ///
+ /// Surfaces the Console warning for dictionary fields that deserialize duplicate-key or null-key rows on a
+ /// serialized-file load or clone (UUM-146883).
+ ///
+ ///
+ /// Dictionary reads run on serialization worker threads, so just posts the
+ /// warning here; the emit is deferred to the main thread, which buys the two things that matter:
+ ///
+ /// - redundant warnings are filtered out -- one operation reads the dict through several transient hosts, but
+ /// only the still-loaded host survives the emit-time liveness check, so it warns once instead of N times; and
+ /// - the host's name can be read (a main-thread-only API) and put in the message, so the user can tell which
+ /// object owns the offending field -- the clickable ping alone can't (e.g. a child of a Prefab Asset only pings the
+ /// asset root).
+ ///
+ /// Only dictionaries surface warnings today, but the deferred-emit pipeline (capture, enqueue, liveness-filter,
+ /// host-resolve, log) is container-neutral. When new containers such as HashSet gain the same warning, this
+ /// class can be extended to support them with minor refactoring: wire their hook in and add
+ /// the per-container message wording alongside .
+ ///
+ internal static partial class DeserializationWarningHandler
+ {
+ // Captured in Initialize; worker threads Post onto it (Current is per-thread, but Post is thread-safe).
+ [NoAutoStaticsCleanup] // re-captured per domain in Initialize on reload
+ static SynchronizationContext s_MainThreadContext;
+
+ // [OnCodeLoaded] runs on the main thread before any worker-thread serialization, so the context is
+ // captured and the hook wired before the first dictionary read can post a warning.
+ [OnCodeLoaded]
+ static void Initialize()
+ {
+ s_MainThreadContext = SynchronizationContext.Current;
+ DictionarySerialization.s_PostDictionaryKeyWarning = Enqueue;
+ }
+
+ // Worker-thread hook: hand the raw ingredients to the main thread for a deferred, filtered emit.
+ static void Enqueue(EntityId hostingEntityId, string fieldIdentifier, bool hadDuplicates, bool hadNullKeys)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(fieldIdentifier));
+ s_MainThreadContext?.Post(Emit, (hostingEntityId, fieldIdentifier, hadDuplicates, hadNullKeys));
+ }
+
+ // Main thread, once the queue next drains: log a single clickable warning per still-loaded host. The
+ // 'Resources.IsInstanceLoaded(host)' check does two things: it dedups the N transient hosts one operation reads
+ // through down to the one still-loaded host (so we warn once, not N times), and it guarantees the logged object
+ // is pingable -- transient import objects (e.g. during Prefab import) are dropped rather than logged unclickable.
+ static void Emit(object state)
+ {
+ var (host, fieldIdentifier, hadDuplicates, hadNullKeys) = ((EntityId, string, bool, bool))state;
+ if (!Resources.IsInstanceLoaded(host))
+ return;
+ var context = Object.FindObjectFromInstanceID(host);
+ string message = ComposeDictionaryMessage(context.name, fieldIdentifier, hadDuplicates, hadNullKeys);
+ Debug.LogFormat(LogType.Warning, LogOption.NoStacktrace, context, "{0}", message);
+ }
+
+ static string ComposeDictionaryMessage(string hostName, string fieldIdentifier, bool hadDuplicates, bool hadNullKeys)
+ {
+ Debug.Assert(hadDuplicates || hadNullKeys);
+ string body = string.Empty;
+ if (hadDuplicates)
+ body = "contains duplicate key entries. Ensure all keys are unique. Only the first occurrence of each key will be added to the dictionary object.";
+ if (hadNullKeys)
+ {
+ if (body.Length > 0)
+ body += " It also ";
+ body += "contains entries with a null key. A dictionary cannot contain a null key, so Unity excludes these entries from the dictionary object.";
+ }
+ return $"Dictionary field '{fieldIdentifier}' on '{hostName}' {body}";
+ }
+ }
+}
diff --git a/Editor/Mono/Serialization/DuplicateEntriesForDictionaries.cs b/Editor/Mono/Serialization/DictionaryIgnoredEntriesCache.cs
similarity index 70%
rename from Editor/Mono/Serialization/DuplicateEntriesForDictionaries.cs
rename to Editor/Mono/Serialization/DictionaryIgnoredEntriesCache.cs
index f054456466..7f8fc8cde1 100644
--- a/Editor/Mono/Serialization/DuplicateEntriesForDictionaries.cs
+++ b/Editor/Mono/Serialization/DictionaryIgnoredEntriesCache.cs
@@ -7,7 +7,7 @@
namespace UnityEngine
{
///
- /// Managed-side storage for duplicate dictionary entries (e.g. duplicate keys) per (instance, property path).
+ /// Managed-side storage for ignored dictionary entries (e.g. duplicate keys, null-key placeholders) per (instance, property path).
/// Outer key is the hosting object's entity id; inner key is the formatted property path for the dictionary field.
/// Editor and Editor play mode only; not included in the player.
///
@@ -15,13 +15,13 @@ namespace UnityEngine
/// ( and
/// ) are reachable from worker
/// threads through the native transfer pipeline, while editor cleanup
- /// ( ) and the public
- /// SerializedProperty.GetDictionaryDuplicateEntryIndices API are invoked from the main thread.
+ /// ( ) and the public
+ /// SerializedProperty.GetDictionaryIgnoredEntries API are invoked from the main thread.
///
- internal sealed class DuplicateEntriesForDictionaries : IDuplicateEntriesForDictionaries
+ internal sealed class DictionaryIgnoredEntriesCache : IDictionaryIgnoredEntriesCache
{
private readonly object m_Lock = new object();
- private readonly Dictionary> m_DuplicateEntriesByHost = new Dictionary>();
+ private readonly Dictionary> m_IgnoredEntriesByHost = new Dictionary>();
public bool HasAnyCachedHosts
{
@@ -29,36 +29,36 @@ public bool HasAnyCachedHosts
{
lock (m_Lock)
{
- return m_DuplicateEntriesByHost.Count > 0;
+ return m_IgnoredEntriesByHost.Count > 0;
}
}
}
- public void Store(EntityId hostId, string dictionaryPath, DuplicateEntriesData duplicateEntriesData)
+ public void Store(EntityId hostId, string dictionaryPath, IgnoredEntriesData ignoredEntriesData)
{
- if (duplicateEntriesData.indices == null || duplicateEntriesData.indices.Length == 0
- || duplicateEntriesData.entries == null || duplicateEntriesData.entries.Length == 0)
+ if (ignoredEntriesData.indices == null || ignoredEntriesData.indices.Length == 0
+ || ignoredEntriesData.entries == null || ignoredEntriesData.entries.Length == 0)
return;
if (hostId == EntityId.None || string.IsNullOrEmpty(dictionaryPath))
return;
lock (m_Lock)
{
- if (!m_DuplicateEntriesByHost.TryGetValue(hostId, out var inner))
+ if (!m_IgnoredEntriesByHost.TryGetValue(hostId, out var inner))
{
- inner = new Dictionary();
- m_DuplicateEntriesByHost[hostId] = inner;
+ inner = new Dictionary();
+ m_IgnoredEntriesByHost[hostId] = inner;
}
- inner[dictionaryPath] = duplicateEntriesData;
+ inner[dictionaryPath] = ignoredEntriesData;
}
}
- public DuplicateEntriesData Get(EntityId hostId, string dictionaryPath)
+ public IgnoredEntriesData Get(EntityId hostId, string dictionaryPath)
{
if (hostId == EntityId.None || string.IsNullOrEmpty(dictionaryPath))
return default;
lock (m_Lock)
{
- if (!m_DuplicateEntriesByHost.TryGetValue(hostId, out var inner))
+ if (!m_IgnoredEntriesByHost.TryGetValue(hostId, out var inner))
return default;
if (!inner.TryGetValue(dictionaryPath, out var data))
return default;
@@ -72,11 +72,11 @@ public void Clear(EntityId hostId, string dictionaryPath)
return;
lock (m_Lock)
{
- if (!m_DuplicateEntriesByHost.TryGetValue(hostId, out var inner))
+ if (!m_IgnoredEntriesByHost.TryGetValue(hostId, out var inner))
return;
inner.Remove(dictionaryPath);
if (inner.Count == 0)
- m_DuplicateEntriesByHost.Remove(hostId);
+ m_IgnoredEntriesByHost.Remove(hostId);
}
}
@@ -98,11 +98,11 @@ public int PruneUnloadedHosts()
// enumerating it; it is not a snapshot of the full host set.
lock (m_Lock)
{
- if (m_DuplicateEntriesByHost.Count == 0)
+ if (m_IgnoredEntriesByHost.Count == 0)
return 0;
List toRemove = null;
- foreach (EntityId hostId in m_DuplicateEntriesByHost.Keys)
+ foreach (EntityId hostId in m_IgnoredEntriesByHost.Keys)
{
if (hostId == EntityId.None || !Resources.IsInstanceLoaded(hostId))
{
@@ -115,18 +115,18 @@ public int PruneUnloadedHosts()
return 0;
foreach (EntityId id in toRemove)
- m_DuplicateEntriesByHost.Remove(id);
+ m_IgnoredEntriesByHost.Remove(id);
return toRemove.Count;
}
}
- public bool HostHasDuplicateDictionaryEntries(EntityId hostId)
+ public bool HostHasIgnoredDictionaryEntries(EntityId hostId)
{
if (hostId == EntityId.None)
return false;
lock (m_Lock)
{
- return m_DuplicateEntriesByHost.TryGetValue(hostId, out var inner) && inner.Count > 0;
+ return m_IgnoredEntriesByHost.TryGetValue(hostId, out var inner) && inner.Count > 0;
}
}
}
diff --git a/Editor/Mono/Serialization/DictionarySerializationDuplicateEntriesCleanup.cs b/Editor/Mono/Serialization/DictionarySerializationIgnoredEntriesCleanup.cs
similarity index 70%
rename from Editor/Mono/Serialization/DictionarySerializationDuplicateEntriesCleanup.cs
rename to Editor/Mono/Serialization/DictionarySerializationIgnoredEntriesCleanup.cs
index bd6f13fd08..2b1010a4c7 100644
--- a/Editor/Mono/Serialization/DictionarySerializationDuplicateEntriesCleanup.cs
+++ b/Editor/Mono/Serialization/DictionarySerializationIgnoredEntriesCleanup.cs
@@ -10,7 +10,7 @@
namespace UnityEditor
{
///
- /// Clears duplicate dictionary serialization cache when hosts are destroyed. Uses
+ /// Clears ignored dictionary serialization cache when hosts are destroyed. Uses
/// for undo-recorded destroys, when a scene is closed in the Editor
/// (including in Edit Mode), and
/// for runtime unloads (e.g. Play Mode). Prunes map entries whose host is no longer in memory.
@@ -18,31 +18,31 @@ namespace UnityEditor
// 'partial' is required by the [OnCodeLoaded] source generator (UAC0031); the
// generated companion holds the registration that wires Initialize() into the
// lifecycle pipeline.
- internal static partial class DictionarySerializationDuplicateEntriesCleanup
+ internal static partial class DictionarySerializationIgnoredEntriesCleanup
{
// [OnCodeLoaded] runs before SerializableManagedRefsUtilities::RestoreBackups, ensuring the
- // managed cache is non-null when restored duplicate dictionary entries are written into it.
+ // managed cache is non-null when restored ignored dictionary entries are written into it.
// The previous [InitializeOnLoad] hook ran *after* RestoreBackups and silently dropped those
// entries on every script-recompile domain reload.
[OnCodeLoaded]
static void Initialize()
{
- EnsureDuplicateEntriesCacheInitialized();
+ EnsureIgnoredEntriesCacheInitialized();
ObjectChangeEvents.changesPublished += OnObjectChanges;
EditorSceneManager.sceneClosed += OnEditorSceneClosed;
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
///
- /// Constructs the editor-only duplicate-entry storage and assigns it to the runtime field.
+ /// Constructs the editor-only ignored-entry storage and assigns it to the runtime field.
/// Idempotent: re-entry leaves the existing instance untouched.
///
- internal static void EnsureDuplicateEntriesCacheInitialized()
+ internal static void EnsureIgnoredEntriesCacheInitialized()
{
- if (DictionarySerialization.s_DuplicateEntriesForDictionaries != null)
+ if (DictionarySerialization.s_IgnoredEntriesForDictionaries != null)
return;
- // Runtime does not construct storage (player stays null); wire the editor-only implementation here so edit mode and play mode in the Editor preserve duplicate dictionary rows.
- DictionarySerialization.s_DuplicateEntriesForDictionaries = new DuplicateEntriesForDictionaries();
+ // Runtime does not construct storage (player stays null); wire the editor-only implementation here so edit mode and play mode in the Editor preserve duplicate-key and null-key dictionary rows.
+ DictionarySerialization.s_IgnoredEntriesForDictionaries = new DictionaryIgnoredEntriesCache();
}
static void OnObjectChanges(ref ObjectChangeEventStream stream)
@@ -58,7 +58,7 @@ static void OnObjectChanges(ref ObjectChangeEventStream stream)
if (!sawDestroy)
return;
- PruneStaleDuplicateHosts();
+ PruneStaleIgnoredHosts();
}
static void OnEditorSceneClosed(Scene scene) => PruneAfterSceneRemovedFromHierarchyIfNeeded();
@@ -67,15 +67,15 @@ static void OnObjectChanges(ref ObjectChangeEventStream stream)
static void PruneAfterSceneRemovedFromHierarchyIfNeeded()
{
- if (!DictionarySerialization.HasAnyCachedDuplicateDictionaryHosts())
+ if (!DictionarySerialization.HasAnyCachedIgnoredDictionaryHosts())
return;
- PruneStaleDuplicateHosts();
+ PruneStaleIgnoredHosts();
}
- static void PruneStaleDuplicateHosts()
+ static void PruneStaleIgnoredHosts()
{
- DictionarySerialization.PruneDuplicateDictionaryEntriesForUnloadedHosts();
+ DictionarySerialization.PruneIgnoredDictionaryEntriesForUnloadedHosts();
}
}
}
diff --git a/Editor/Mono/SerializedProperty.bindings.cs b/Editor/Mono/SerializedProperty.bindings.cs
index 41bc16d17d..75e2c2e29b 100644
--- a/Editor/Mono/SerializedProperty.bindings.cs
+++ b/Editor/Mono/SerializedProperty.bindings.cs
@@ -796,13 +796,44 @@ public int[] GetDictionaryDuplicateEntryIndices()
EntityId entityId = EntityId.None;
if (serializedObject?.targetObject is UnityEngine.Object target)
entityId = target.GetEntityId();
- if (entityId == EntityId.None || !UnityEngine.DictionarySerialization.HostHasDuplicateDictionaryEntries(entityId))
+ if (entityId == EntityId.None || !UnityEngine.DictionarySerialization.HostHasIgnoredDictionaryEntries(entityId))
return Array.Empty();
- return UnityEngine.DictionarySerialization.GetDuplicateIndices(entityId, GetDictionaryDuplicateLookupIdentifierInternal());
+ // Only the genuine duplicate-key rows are surfaced here; the null-key placeholder rows are discarded.
+ return UnityEngine.DictionarySerialization.GetIgnoredEntryIndices(
+ entityId, GetDictionaryIgnoredEntryIdentifierInternal()).duplicateEntryIndices;
}
- [NativeName("GetDictionaryDuplicateLookupIdentifier")]
- private extern string GetDictionaryDuplicateLookupIdentifierInternal();
+ [NativeName("GetDictionaryIgnoredEntryIdentifier")]
+ private extern string GetDictionaryIgnoredEntryIdentifierInternal();
+
+ ///
+ /// Returns, in a single pass, the dictionary's duplicate-key and null-key placeholder rows -- both shown by
+ /// the inspector but excluded from the runtime dictionary. Prefer this over
+ /// plus a separate null-key query when both sets are needed:
+ /// resolving the ignored-entry identifier is the expensive part and this does it only once.
+ ///
+ ///
+ /// Thrown when the underlying represents multiple selected targets. Multi-selection of dictionaries is not supported.
+ ///
+ public DictionaryIgnoredEntries GetDictionaryIgnoredEntries()
+ {
+ Verify(VerifyFlags.None);
+ if (serializedObject != null && serializedObject.targetObjectsCount > 1)
+ throw new InvalidOperationException(
+ "GetDictionaryIgnoredEntries is not supported on a SerializedObject that represents multiple selected targets.");
+
+ UnityEngine.Debug.Assert(propertyType == SerializedPropertyType.Generic,
+ $"GetDictionaryIgnoredEntries was called on property '{propertyPath}' whose type is '{propertyType}'. This API is only valid on a Dictionary<,> field (which surfaces as SerializedPropertyType.Generic).");
+
+ EntityId entityId = EntityId.None;
+ if (serializedObject?.targetObject is UnityEngine.Object target)
+ entityId = target.GetEntityId();
+ if (entityId == EntityId.None || !UnityEngine.DictionarySerialization.HostHasIgnoredDictionaryEntries(entityId))
+ return DictionaryIgnoredEntries.Empty;
+
+ return UnityEngine.DictionarySerialization.GetIgnoredEntryIndices(
+ entityId, GetDictionaryIgnoredEntryIdentifierInternal());
+ }
// Returns an FNV-1a 64-bit combination of GetContentHash() for every dictionary
// entry's key. Receiver must be the inner Array property of a serialized dictionary
diff --git a/Editor/Mono/Settings.cs b/Editor/Mono/Settings.cs
index 1950ca716c..92fafd9ca9 100644
--- a/Editor/Mono/Settings.cs
+++ b/Editor/Mono/Settings.cs
@@ -267,129 +267,79 @@ static void Load()
}
}
- internal class SavedInt
+ internal abstract class SavedValue where T : IEquatable
{
- int m_Value;
- string m_Name;
+ protected T m_Value;
+ protected string m_Name;
bool m_Loaded;
- public SavedInt(string name, int value)
+ public event Action valueChanged;
+
+ protected SavedValue(string name, T defaultValue)
{
m_Name = name;
- m_Loaded = false;
- m_Value = value;
+ m_Value = defaultValue;
}
+ protected abstract T ReadFromPrefs(T defaultValue);
+ protected abstract void WriteToPrefs(T value);
+
private void Load()
{
if (m_Loaded)
return;
m_Loaded = true;
- m_Value = EditorPrefs.GetInt(m_Name, m_Value);
+ m_Value = ReadFromPrefs(m_Value);
}
- public int value
+ public T value
{
get { Load(); return m_Value; }
set
{
Load();
- if (m_Value == value)
+ if (m_Value.Equals(value))
return;
m_Value = value;
- EditorPrefs.SetInt(m_Name, value);
+ WriteToPrefs(value);
+ valueChanged?.Invoke();
}
}
- public static implicit operator int(SavedInt s)
+ // Re-reads the pref store and fires valueChanged if the value has changed since last load.
+ // A no-op if the value has never been read (the next access will pick up the current store value).
+ public void Refresh()
{
- return s.value;
+ if (!m_Loaded) return;
+ var fresh = ReadFromPrefs(m_Value);
+ if (fresh.Equals(m_Value)) return;
+ m_Value = fresh;
+ valueChanged?.Invoke();
}
}
- internal class SavedFloat
+ internal class SavedInt : SavedValue
{
- float m_Value;
- string m_Name;
- bool m_Loaded;
-
- public SavedFloat(string name, float value)
- {
- m_Name = name;
- m_Loaded = false;
- m_Value = value;
- }
-
- private void Load()
- {
- if (m_Loaded)
- return;
-
- m_Loaded = true;
- m_Value = EditorPrefs.GetFloat(m_Name, m_Value);
- }
-
- public float value
- {
- get { Load(); return m_Value; }
- set
- {
- Load();
- if (m_Value == value)
- return;
- m_Value = value;
- EditorPrefs.SetFloat(m_Name, value);
- }
- }
-
- public static implicit operator float(SavedFloat s)
- {
- return s.value;
- }
+ public SavedInt(string name, int value) : base(name, value) { }
+ protected override int ReadFromPrefs(int defaultValue) => EditorPrefs.GetInt(m_Name, defaultValue);
+ protected override void WriteToPrefs(int value) => EditorPrefs.SetInt(m_Name, value);
+ public static implicit operator int(SavedInt s) => s.value;
}
- internal class SavedBool
+ internal class SavedFloat : SavedValue
{
- bool m_Value;
- string m_Name;
- bool m_Loaded;
-
- public SavedBool(string name, bool value)
- {
- m_Name = name;
- m_Loaded = false;
- m_Value = value;
- }
-
- public event Action valueChanged;
-
- private void Load()
- {
- if (m_Loaded)
- return;
-
- m_Loaded = true;
- m_Value = EditorPrefs.GetBool(m_Name, m_Value);
- }
-
- public bool value
- {
- get { Load(); return m_Value; }
- set
- {
- Load();
- if (m_Value == value)
- return;
- m_Value = value;
- EditorPrefs.SetBool(m_Name, value);
- valueChanged?.Invoke();
- }
- }
+ public SavedFloat(string name, float value) : base(name, value) { }
+ protected override float ReadFromPrefs(float defaultValue) => EditorPrefs.GetFloat(m_Name, defaultValue);
+ protected override void WriteToPrefs(float value) => EditorPrefs.SetFloat(m_Name, value);
+ public static implicit operator float(SavedFloat s) => s.value;
+ }
- public static implicit operator bool(SavedBool s)
- {
- return s.value;
- }
+ internal class SavedBool : SavedValue
+ {
+ public SavedBool(string name, bool value) : base(name, value) { }
+ protected override bool ReadFromPrefs(bool defaultValue) => EditorPrefs.GetBool(m_Name, defaultValue);
+ protected override void WriteToPrefs(bool value) => EditorPrefs.SetBool(m_Name, value);
+ public static implicit operator bool(SavedBool s) => s.value;
}
}
diff --git a/Editor/Mono/SettingsWindow/LightingEditor.cs b/Editor/Mono/SettingsWindow/LightingEditor.cs
index 6eaebca8c8..47309a80a8 100644
--- a/Editor/Mono/SettingsWindow/LightingEditor.cs
+++ b/Editor/Mono/SettingsWindow/LightingEditor.cs
@@ -42,6 +42,7 @@ static Styles() {}
public static readonly GUIContent ambientDown = EditorGUIUtility.TrTextContent("Ground Color", "Controls the color of light emitted from the ground of the Scene.");
public static readonly GUIContent ambient = EditorGUIUtility.TrTextContent("Ambient Color", "Controls the color of the ambient light contributed to the Scene.");
public static readonly GUIContent customReflection = EditorGUIUtility.TrTextContent("Cubemap", "Specifies the custom cube map used for reflection effects in the Scene.");
+ public static readonly GUIContent unusedCustomReflectionWarning = EditorGUIUtility.TrTextContent("A custom cubemap is still assigned and included in builds, even though Source is set to Skybox. To remove it, set Source to Custom and clear the Cubemap field.");
public static readonly GUIContent SubtractiveColor = EditorGUIUtility.TrTextContent("Realtime Shadow Color", "The color used for mixing realtime shadows with baked lightmaps in Subtractive lighting mode. The color defines the darkest point of the realtime shadow.");
public static readonly GUIContent[] kFullAmbientSource =
@@ -130,6 +131,16 @@ public virtual void OnDisable()
SessionState.SetBool(kShowEnvironment, m_bShowEnvironment);
}
+ // Match the RenderSettings.customReflectionTexture setter, which rejects non-cube textures.
+ static Object CustomReflectionValidator(Object[] references, System.Type objType, SerializedProperty property, EditorGUI.ObjectFieldValidatorOptions options)
+ {
+ var texture = EditorGUI.ValidateObjectFieldAssignment(references, typeof(Texture), property, options) as Texture;
+ if (texture == null || texture.dimension == TextureDimension.Cube)
+ return texture;
+ // A non-cube pick keeps the current value; only an explicit None clears the field.
+ return property.objectReferenceValue;
+ }
+
private void DrawGUI()
{
Material skyboxMaterial = m_SkyboxMaterial.objectReferenceValue as Material;
@@ -204,11 +215,7 @@ private void DrawGUI()
EditorGUILayout.LabelField(Styles.env_refl_top);
EditorGUI.indentLevel++;
- EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_DefaultReflectionMode, Styles.env_refl_src);
- if (EditorGUI.EndChangeCheck())
- if ((DefaultReflectionMode)m_DefaultReflectionMode.intValue == DefaultReflectionMode.FromSkybox)
- m_CustomReflection.objectReferenceValue = null;
DefaultReflectionMode defReflectionMode = (DefaultReflectionMode)m_DefaultReflectionMode.intValue;
switch (defReflectionMode)
@@ -219,10 +226,12 @@ private void DrawGUI()
GUIContent[] reflectionResolutionTextArray = null;
ReflectionProbeEditor.GetResolutionArray(ref reflectionResolutionValuesArray, ref reflectionResolutionTextArray);
EditorGUILayout.IntPopup(m_DefaultReflectionResolution, reflectionResolutionTextArray, reflectionResolutionValuesArray, Styles.env_refl_res, GUILayout.MinWidth(40));
+ if (m_CustomReflection.objectReferenceValue != null)
+ EditorGUILayout.HelpBox(Styles.unusedCustomReflectionWarning.text, MessageType.Warning);
}
break;
case DefaultReflectionMode.Custom:
- EditorGUILayout.PropertyField(m_CustomReflection, Styles.customReflection);
+ EditorGUILayout.ObjectField(m_CustomReflection, typeof(Texture), Styles.customReflection, CustomReflectionValidator);
break;
}
diff --git a/Editor/Mono/ShaderUtil.bindings.cs b/Editor/Mono/ShaderUtil.bindings.cs
index 971403521f..5a2b13fbd4 100644
--- a/Editor/Mono/ShaderUtil.bindings.cs
+++ b/Editor/Mono/ShaderUtil.bindings.cs
@@ -289,6 +289,7 @@ public static bool IsGraphicsAPISupported(ComputeShader shader, GraphicsDeviceTy
[FreeFunction] extern internal static int GetSubshaderLOD([NotNull] Shader shader, int subShaderIndex);
[FreeFunction] extern internal static bool IsGrabPass([NotNull] Shader shader, int subShaderIndex, int passId);
[FreeFunction("ShaderUtil::GetShaderSerializedSubshaderCount")] extern internal static int GetShaderSerializedSubshaderCount([NotNull] Shader shader);
+ [FreeFunction("ShaderUtil::GetSerializedSubShaderStripFromBuild")] extern internal static bool GetSerializedSubShaderStripFromBuild([NotNull] Shader shader, int subShaderIndex);
[FreeFunction("ShaderUtil::FindSerializedSubShaderTagValue")] extern internal static int FindSerializedSubShaderTagValue([NotNull] Shader shader, int subShaderIndex, int tagName);
[FreeFunction("ShaderUtil::FindPassTagValue")] extern internal static int FindPassTagValue([NotNull] Shader shader, int subShaderIndex, int passIndex, int tagName);
diff --git a/Editor/Mono/Shaders/RenderPipelineSubShaderStripping.cs b/Editor/Mono/Shaders/RenderPipelineSubShaderStripping.cs
new file mode 100644
index 0000000000..f018a6324e
--- /dev/null
+++ b/Editor/Mono/Shaders/RenderPipelineSubShaderStripping.cs
@@ -0,0 +1,155 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using System;
+using System.Collections.Generic;
+using UnityEditorInternal;
+using UnityEngine;
+using UnityEngine.Rendering;
+using UnityEngine.Scripting;
+
+namespace UnityEditor.Shaders
+{
+ [RequiredByNativeCode]
+ internal static class RenderPipelineSubShaderStripping
+ {
+ const string k_GraphicsSettingsPath = "ProjectSettings/GraphicsSettings.asset";
+ const string k_QualitySettingsPath = "ProjectSettings/QualitySettings.asset";
+
+ // RenderPipeline shader tags for the persisted RP configuration of the build target group.
+ // non-empty: resolved tag set. empty: no RP configured, strip tagged subshaders (UUM-141340).
+ // null: an RP asset is referenced but not imported yet (cold Library) - don't strip/cache, let
+ // the tag custom dependency reimport the shader once it resolves.
+ [RequiredByNativeCode]
+ internal static string[] GetActiveRenderPipelineShaderTagsForPlatform(string buildTargetGroupName)
+ {
+ var unique = UnityEngine.Pool.HashSetPool.Get();
+ try
+ {
+ // Typed overload avoids substituting the live (runtime-overridable) default RP.
+ QualitySettings.GetRenderPipelineAssetsForPlatform(
+ buildTargetGroupName, out HashSet perQualityAssets, out bool allLevelsAreOverridden);
+
+ foreach (var asset in perQualityAssets)
+ AddTagIfPersistent(unique, asset);
+
+ // Typed overload above drops not-yet-imported per-quality assets, so check them directly.
+ if (AnyPersistentPerQualityRenderPipelineUnresolved())
+ return null;
+
+ if (!allLevelsAreOverridden || perQualityAssets.Count == 0)
+ {
+ if (!TryAddPersistentDefaultRenderPipelineTag(unique))
+ return null; // referenced but not imported yet
+ }
+
+ if (unique.Count == 0)
+ return Array.Empty();
+
+ var result = new string[unique.Count];
+ unique.CopyTo(result);
+ return result;
+ }
+ finally
+ {
+ UnityEngine.Pool.HashSetPool.Release(unique);
+ }
+ }
+
+ static void AddTagIfPersistent(HashSet tags, RenderPipelineAsset asset)
+ {
+ // In-memory assets (e.g. test pipelines) can't be part of a build; ignore for stable results.
+ if (asset == null || !EditorUtility.IsPersistent(asset))
+ return;
+
+ string tag = asset.renderPipelineShaderTag;
+ if (!string.IsNullOrEmpty(tag))
+ tags.Add(tag);
+ }
+
+ // Reads the default RP from disk (not the runtime-overridable live one). Returns false when it's
+ // referenced but not importable yet (unresolved); true when resolved or genuinely unconfigured.
+ static bool TryAddPersistentDefaultRenderPipelineTag(HashSet tags)
+ {
+ return InspectPersistentSettings(k_GraphicsSettingsPath, ifMissing: true, loaded =>
+ {
+ foreach (var obj in loaded)
+ {
+ if (obj == null)
+ continue;
+
+ var property = new SerializedObject(obj).FindProperty("m_CustomRenderPipeline");
+ if (property == null)
+ continue;
+
+ var asset = property.objectReferenceValue as RenderPipelineAsset;
+ if (asset != null)
+ {
+ AddTagIfPersistent(tags, asset);
+ return true;
+ }
+
+ return !IsUnresolvedRenderPipelineReference(property);
+ }
+ return true;
+ });
+ }
+
+ // Conservative across all quality levels (not just the current platform's): worst case is one
+ // extra reimport cycle, never a wrong strip.
+ static bool AnyPersistentPerQualityRenderPipelineUnresolved()
+ {
+ return InspectPersistentSettings(k_QualitySettingsPath, ifMissing: false, loaded =>
+ {
+ foreach (var obj in loaded)
+ {
+ if (obj == null)
+ continue;
+
+ var levels = new SerializedObject(obj).FindProperty("m_QualitySettings");
+ if (levels == null || !levels.isArray)
+ continue;
+
+ for (int i = 0; i < levels.arraySize; ++i)
+ {
+ var rp = levels.GetArrayElementAtIndex(i).FindPropertyRelative("customRenderPipeline");
+ if (rp != null && IsUnresolvedRenderPipelineReference(rp))
+ return true;
+ }
+ return false;
+ }
+ return false;
+ });
+ }
+
+ // Loads a persisted settings file as detached copies, passes them to 'inspect', and always
+ // destroys them. Returns 'ifMissing' when the file can't be loaded.
+ static bool InspectPersistentSettings(string path, bool ifMissing, Func inspect)
+ {
+ UnityEngine.Object[] loaded = InternalEditorUtility.LoadSerializedFileAndForget(path);
+ if (loaded == null)
+ return ifMissing;
+
+ try
+ {
+ return inspect(loaded);
+ }
+ finally
+ {
+ // Detached copies are owned by us.
+ foreach (var obj in loaded)
+ {
+ if (obj != null)
+ UnityEngine.Object.DestroyImmediate(obj);
+ }
+ }
+ }
+
+ // Null object with a live entity id == referenced but not imported yet.
+ static bool IsUnresolvedRenderPipelineReference(SerializedProperty property)
+ {
+ return property.objectReferenceValue == null && property.objectReferenceEntityIdValue != EntityId.None;
+ }
+ }
+}
diff --git a/Editor/Mono/Sprites/SpriteUtilityWindow.cs b/Editor/Mono/Sprites/SpriteUtilityWindow.cs
index 5dddfdd8c7..4a0b3b91c6 100644
--- a/Editor/Mono/Sprites/SpriteUtilityWindow.cs
+++ b/Editor/Mono/Sprites/SpriteUtilityWindow.cs
@@ -24,11 +24,17 @@ protected class Styles
static LaunchSpriteEditorWindowAfterDomainReload s_LaunchSpriteEditorWindowAfterDomainReload;
- internal static bool DoOpenSpriteEditorWindowUI()
+ internal static bool DoOpenSpriteEditorWindowUI(bool enableOpenSpriteEditorButton, bool enableInstallButton = true)
{
var buttonText = showSpriteEditorWindow == null ? Styles.install2DPackage : Styles.openSpriteEditor;
GUILayout.BeginVertical();
- var clicked = GUILayout.Button(buttonText);
+ var clicked = false;
+ using (new EditorGUI.DisabledScope((showSpriteEditorWindow != null && !enableOpenSpriteEditorButton) ||
+ (showSpriteEditorWindow == null && !enableInstallButton)))
+ {
+ clicked = GUILayout.Button(buttonText);
+ }
+
if (showSpriteEditorWindow == null)
EditorGUILayout.HelpBox(Styles.install2DPackageReason.text, MessageType.Info, true);
GUILayout.EndVertical();
diff --git a/Editor/Mono/Text/EditorTextSettings.cs b/Editor/Mono/Text/EditorTextSettings.cs
index e7c807b3f5..2b5c31ad04 100644
--- a/Editor/Mono/Text/EditorTextSettings.cs
+++ b/Editor/Mono/Text/EditorTextSettings.cs
@@ -48,6 +48,26 @@ static EditorTextSettings()
UITKTextHandle.GenerateBitmapFallbackFontAssets = CanGenerateFallbackFontAssets;
}
+ [InitializeOnLoadMethod]
+ static void InitializeDefaultTextSettings()
+ {
+ if (EditorApplication.isBuildingAnyResources)
+ return;
+
+ // Force init after each domain reload (lazy init may hit a worker thread).
+ _ = defaultTextSettings;
+ }
+
+ // Persisted across domain reloads; re-attached to the base runtime caches via UsePersistedCaches.
+ [SerializeField, HideInInspector]
+ List m_PersistedFontReferences = new List(); // -> instance m_FontReferences
+ [SerializeField, HideInInspector]
+ List m_PersistedFallbackOSFontAssets = new List(); // -> instance m_FallbackOSFontAssets
+ [SerializeField, HideInInspector]
+ List m_PersistedGlobalOSFallbacks = new List(); // -> static global OS fallback store
+
+ internal override bool persistsFontAssetCaches => true;
+
private void OnEnable()
{
// We cannot rely on lazy initialization since they might be called on a thread, which isn't valid
@@ -134,6 +154,14 @@ internal static EditorTextSettings defaultTextSettings
s_DefaultTextSettings = EditorGUIUtility.Load(s_DefaultEditorTextSettingPath) as EditorTextSettings;
if (s_DefaultTextSettings)
{
+ s_DefaultTextSettings.m_PersistedFontReferences ??= new List();
+ s_DefaultTextSettings.m_PersistedFallbackOSFontAssets ??= new List();
+ s_DefaultTextSettings.m_PersistedGlobalOSFallbacks ??= new List();
+ s_DefaultTextSettings.UsePersistedCaches(
+ s_DefaultTextSettings.m_PersistedFontReferences,
+ s_DefaultTextSettings.m_PersistedFallbackOSFontAssets,
+ s_DefaultTextSettings.m_PersistedGlobalOSFallbacks);
+
UpdateLocalizationFontAsset();
UpdateDefaultTextStyleSheet();
s_DefaultTextSettings.CreateDefaultEditorFontAsset();
diff --git a/Editor/Mono/Tools/EditorAction.cs b/Editor/Mono/Tools/EditorAction.cs
index 67e3ca4534..ea54fd9ca6 100644
--- a/Editor/Mono/Tools/EditorAction.cs
+++ b/Editor/Mono/Tools/EditorAction.cs
@@ -3,7 +3,6 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
-using UnityEngine;
using UnityEditor.EditorTools;
namespace UnityEditor.Actions
@@ -26,10 +25,10 @@ public static T Start(T action) where T : EditorAction
{
return Start(action, typeof(SceneView));
}
-
- internal static T Start(Type toolOwner) where T : EditorAction, new() => Start(new T(), toolOwner);
- internal static T Start(T action, Type toolOwner) where T : EditorAction
+ internal static T Start(Type toolOwnerType) where T : EditorAction, new() => Start(new T(), toolOwnerType);
+
+ internal static T Start(T action, Type toolOwnerType) where T : EditorAction
{
if (action == null)
throw new ArgumentNullException(nameof(action));
@@ -37,14 +36,14 @@ internal static T Start(T action, Type toolOwner) where T : EditorAction
if (action.m_IsFinished)
return action;
- EditorToolManager.SetActiveOverride(new EditorActionTool(action, toolOwner), toolOwner);
-
+ EditorToolManager.SetActiveOverride(new EditorActionTool(action, toolOwnerType), toolOwnerType);
+
return action;
}
public virtual void OnSceneGUI(SceneView sceneView) {}
-
- internal virtual void OnToolOwnerGUI(EditorToolWindowBase toolOwnerWindow) {}
+
+ internal virtual void OnEditorToolWindowGUI(EditorWindow editorToolWindow) {}
public void Finish(EditorActionResult result)
{
diff --git a/Editor/Mono/Tools/EditorActionTool.cs b/Editor/Mono/Tools/EditorActionTool.cs
index eeae115528..fa37f337af 100644
--- a/Editor/Mono/Tools/EditorActionTool.cs
+++ b/Editor/Mono/Tools/EditorActionTool.cs
@@ -48,8 +48,8 @@ public void OnGUI(EditorWindow window)
if (window is SceneView sceneView)
action?.OnSceneGUI(sceneView);
- if (window is EditorToolWindowBase toolOwnerWindow)
- action?.OnToolOwnerGUI(toolOwnerWindow);
+ else if (window is ISupportsEditorTools)
+ action?.OnEditorToolWindowGUI(window);
}
void OnActionFinished(EditorActionResult result) => Dispose();
diff --git a/Editor/Mono/Tools/EditorPivotManager.cs b/Editor/Mono/Tools/EditorPivotManager.cs
index d93cd9fbe5..3c749574f2 100644
--- a/Editor/Mono/Tools/EditorPivotManager.cs
+++ b/Editor/Mono/Tools/EditorPivotManager.cs
@@ -128,6 +128,7 @@ public void SetActivePivotMode(Type pivotModeType)
var toolManagerState = EditorToolManager.instance.GetOrCreateStateForType(stateToolOwnerType);
if (pivotModeType == null)
{
+ m_ActivePivotMode?.OnWillBeDeactivated();
m_ActivePivotMode = null;
if (toolManagerState != null)
toolManagerState.pivotMode = PivotMode.Custom;
@@ -137,7 +138,9 @@ public void SetActivePivotMode(Type pivotModeType)
CheckAndThrowIfTypeIncompatible(pivotModeType, typeof(CustomPivotMode), stateToolOwnerType);
if (toolManagerState != null)
{
+ m_ActivePivotMode?.OnWillBeDeactivated();
m_ActivePivotMode = (CustomPivotMode)toolManagerState.GetSingleton(pivotModeType);
+ m_ActivePivotMode.OnActivated();
if (IsBuiltInPivotMode(m_ActivePivotMode))
m_LastBuiltInPivotMode = m_ActivePivotMode;
@@ -165,6 +168,7 @@ public void SetActivePivotRotation(Type pivotRotationType)
var toolManagerState = EditorToolManager.instance.GetOrCreateStateForType(stateToolOwnerType);
if (pivotRotationType == null)
{
+ m_ActivePivotRotation?.OnWillBeDeactivated();
m_ActivePivotRotation = null;
if (toolManagerState != null)
toolManagerState.pivotRotation = PivotRotation.Custom;
@@ -174,7 +178,9 @@ public void SetActivePivotRotation(Type pivotRotationType)
CheckAndThrowIfTypeIncompatible(pivotRotationType, typeof(CustomPivotRotation), stateToolOwnerType);
if (toolManagerState != null)
{
+ m_ActivePivotRotation?.OnWillBeDeactivated();
m_ActivePivotRotation = (CustomPivotRotation)toolManagerState.GetSingleton(pivotRotationType);
+ m_ActivePivotRotation.OnActivated();
if (IsBuiltInPivotRotation(m_ActivePivotRotation))
m_LastBuiltInPivotRotation = m_ActivePivotRotation;
diff --git a/Editor/Mono/Tools/EditorTool.cs b/Editor/Mono/Tools/EditorTool.cs
index c346b4ba86..612669fe0d 100644
--- a/Editor/Mono/Tools/EditorTool.cs
+++ b/Editor/Mono/Tools/EditorTool.cs
@@ -16,7 +16,13 @@ public interface IDrawSelectedHandles
void OnDrawHandles();
}
- public abstract class EditorTool : ScriptableObject, IEditor
+ interface IHasToolOwner
+ {
+ Type toolOwnerType { get; }
+ void SetToolOwner(Type ownerType);
+ }
+
+ public abstract class EditorTool : ScriptableObject, IEditor, IHasToolOwner
{
bool m_Active;
@@ -32,6 +38,21 @@ public abstract class EditorTool : ScriptableObject, IEditor
[SerializeField]
bool m_Hidden;
+ [HideInInspector]
+ [SerializeField]
+ string m_ToolOwnerTypeName;
+
+ Type m_ToolOwnerType;
+
+ Type IHasToolOwner.toolOwnerType
+ {
+ get
+ {
+ m_ToolOwnerType = EditorToolUtility.ResolveToolOwnerType(m_ToolOwnerType, m_ToolOwnerTypeName);
+ return m_ToolOwnerType;
+ }
+ }
+
public IEnumerable targets
{
get
@@ -63,15 +84,15 @@ public virtual bool gridSnapEnabled
}
public bool isHidden => m_Hidden;
-
+
internal static event Action stateChanged;
- internal void Activate(Type toolOwnerType)
+ internal void Activate()
{
if(m_Active
// Prevent to reenable the tool if this is not the active one anymore
// Can happen when entering playmode due to the delayCall in EditorToolManager.OnEnable
- || this != EditorToolManager.GetActiveTool(toolOwnerType))
+ || this != EditorToolManager.GetActiveTool(((IHasToolOwner)this).toolOwnerType))
return;
OnActivated();
@@ -99,7 +120,7 @@ public virtual bool IsAvailable()
{
return true;
}
-
+
public void SetHidden(bool hidden)
{
m_Hidden = hidden;
@@ -115,5 +136,11 @@ void IEditor.SetTargets(UnityObject[] value)
{
m_Targets = value;
}
+
+ void IHasToolOwner.SetToolOwner(Type ownerType)
+ {
+ m_ToolOwnerType = ownerType;
+ m_ToolOwnerTypeName = ownerType?.AssemblyQualifiedName;
+ }
}
}
diff --git a/Editor/Mono/Tools/EditorToolAttributes.cs b/Editor/Mono/Tools/EditorToolAttributes.cs
index 5809f3f864..ce61cb02a8 100644
--- a/Editor/Mono/Tools/EditorToolAttributes.cs
+++ b/Editor/Mono/Tools/EditorToolAttributes.cs
@@ -9,7 +9,7 @@ namespace UnityEditor.EditorTools
public abstract class ToolAttribute : Attribute
{
public const int defaultPriority = 1000;
-
+
string m_DisplayName;
Type m_TargetContext, m_TargetType;
Type m_VariantGroup;
@@ -47,29 +47,25 @@ public Type variantGroup
get => m_VariantGroup;
set => m_VariantGroup = value;
}
-
+
public Type group
{
get => m_Group;
set => m_Group = value;
}
-
+
public int variantPriority
{
get => m_VariantPriority;
set => m_VariantPriority = value;
}
-
+
public bool allowPersistentTargets
{
get => m_AllowPersistentTargets;
set => m_AllowPersistentTargets = value;
}
-
- // This is temporarily internal until it's moved to public API
- Type m_TargetToolOwner;
- internal Type targetToolOwner { get => m_TargetToolOwner; set => m_TargetToolOwner = value; }
-
+
ToolAttribute() {}
protected ToolAttribute(string displayName, Type targetType = null, Type editorToolContext = null)
@@ -128,24 +124,24 @@ public EditorToolAttribute(
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class EditorToolContextAttribute : ToolAttribute
{
- public EditorToolContextAttribute(string displayName = "", Type targetType = null)
- : base(displayName, targetType) {}
-
- internal EditorToolContextAttribute(Type targetToolOwner, string displayName = "")
- : base(displayName, null)
+ Type m_TargetToolOwner;
+ public Type targetToolOwner
{
- this.targetToolOwner = targetToolOwner;
+ get => m_TargetToolOwner;
+ set => m_TargetToolOwner = value;
}
- }
+ public EditorToolContextAttribute(string displayName = "", Type targetType = null)
+ : base(displayName, targetType) {}
+ }
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
- class EditorToolOwnerAttribute : Attribute
+ public class EditorToolOwnerAttribute : Attribute
{
Type m_DefaultContext;
-
+
public Type defaultContext => m_DefaultContext;
-
+
public EditorToolOwnerAttribute(Type defaultContextType)
{
m_DefaultContext = defaultContextType;
diff --git a/Editor/Mono/Tools/EditorToolContext.cs b/Editor/Mono/Tools/EditorToolContext.cs
index d30748178d..785feb2b0d 100644
--- a/Editor/Mono/Tools/EditorToolContext.cs
+++ b/Editor/Mono/Tools/EditorToolContext.cs
@@ -28,7 +28,7 @@ public override VisualElement CreateInspectorGUI()
}
}
- public abstract class EditorToolContext : ScriptableObject, IEditor
+ public abstract class EditorToolContext : ScriptableObject, IEditor, IHasToolOwner
{
bool m_Active;
@@ -57,26 +57,22 @@ public virtual bool overridesDefaultSelection
string m_ContextOwnerTypeName;
Type m_ContextOwnerType;
- Type contextOwnerType
+
+ Type IHasToolOwner.toolOwnerType
{
get
{
- if (m_ContextOwnerType == null && !String.IsNullOrEmpty(m_ContextOwnerTypeName))
- m_ContextOwnerType = Type.GetType(m_ContextOwnerTypeName);
-
- if (m_ContextOwnerType == null)
- m_ContextOwnerType = typeof(SceneView);
-
+ m_ContextOwnerType = EditorToolUtility.ResolveToolOwnerType(m_ContextOwnerType, m_ContextOwnerTypeName);
return m_ContextOwnerType;
}
}
-
+
internal void Activate()
{
if(m_Active
// Prevent to reenable the context if this is not the active one anymore
// Can happen when entering playmode due to the delayCall in EditorToolManager.OnEnable
- || this != EditorToolManager.GetActiveToolContext(contextOwnerType))
+ || this != EditorToolManager.GetActiveToolContext(((IHasToolOwner)this).toolOwnerType))
return;
OnActivated();
@@ -102,7 +98,11 @@ public virtual void PopulateMenu(DropdownMenu menu) {}
void IEditor.SetTargets(UnityObject[] value) => m_Targets = value;
- internal void SetContextOwner(Type contextOwnerType) => m_ContextOwnerTypeName = contextOwnerType.AssemblyQualifiedName;
+ void IHasToolOwner.SetToolOwner(Type ownerType)
+ {
+ m_ContextOwnerType = ownerType;
+ m_ContextOwnerTypeName = ownerType?.AssemblyQualifiedName;
+ }
public virtual void OnToolGUI(EditorWindow window) {}
@@ -114,14 +114,15 @@ public Type ResolveTool(Tool tool)
return typeof(NoneTool);
case Tool.View:
- var toolOwnerIsNullOrSceneView = contextOwnerType == null || contextOwnerType == typeof(SceneView);
+ var ownerType = ((IHasToolOwner)this).toolOwnerType;
+ var toolOwnerIsNullOrSceneView = (ownerType == null || ownerType == typeof(SceneView));
// Do not allow overriding ViewTool if context owner is SceneView
- if (toolOwnerIsNullOrSceneView)
+ if (toolOwnerIsNullOrSceneView)
return typeof(ViewModeTool);
-
+
// Try resolving for custom owner
return DoResolveTool(tool);
-
+
case Tool.Custom:
return null;
@@ -129,7 +130,7 @@ public Type ResolveTool(Tool tool)
return DoResolveTool(tool);
}
}
-
+
Type DoResolveTool(Tool tool)
{
var resolved = GetEditorToolType(tool);
@@ -150,7 +151,8 @@ protected virtual Type GetEditorToolType(Tool tool)
switch (tool)
{
case Tool.View:
- if (contextOwnerType == null || contextOwnerType == typeof(SceneView))
+ var ownerType = ((IHasToolOwner)this).toolOwnerType;
+ if (ownerType == null || ownerType == typeof(SceneView))
throw new ArgumentException(k_ExceptionMsg);
return typeof(ViewModeTool);
case Tool.Move:
diff --git a/Editor/Mono/Tools/EditorToolManager.cs b/Editor/Mono/Tools/EditorToolManager.cs
index 95f565d0a8..234751a580 100644
--- a/Editor/Mono/Tools/EditorToolManager.cs
+++ b/Editor/Mono/Tools/EditorToolManager.cs
@@ -284,7 +284,11 @@ internal EditorTool activeTool
}
m_ActiveTool = tool;
- m_ActiveTool.Activate(stateToolOwnerType);
+
+ if (m_ActiveTool is IHasToolOwner toolWithOwner)
+ toolWithOwner.SetToolOwner(stateToolOwnerType);
+
+ m_ActiveTool.Activate();
ToolManager.ActiveToolDidChange(stateToolOwnerType);
@@ -348,6 +352,9 @@ internal EditorToolContext activeToolContext
ToolManager.ActiveContextWillChange(stateToolOwnerType);
m_ActiveToolContext = ctx;
+ if (ctx is IHasToolOwner ctxWithOwner)
+ ctxWithOwner.SetToolOwner(stateToolOwnerType);
+
ctx.Activate();
RebuildAvailableTools();
@@ -408,7 +415,7 @@ public override void OnEnable()
AssemblyReloadEvents.beforeAssemblyReload += BeforeAssemblyReload;
if (activeTool != null)
- EditorApplication.delayCall += () => activeTool.Activate(stateToolOwnerType);
+ EditorApplication.delayCall += () => activeTool.Activate();
if (activeToolContext != null)
EditorApplication.delayCall += () => activeToolContext.Activate();
}
@@ -685,16 +692,16 @@ public ScriptableObject GetSingleton(Type type)
if (res != null)
{
- if (res is EditorToolContext ctx)
- ctx.SetContextOwner(stateToolOwnerType);
+ if (res is IHasToolOwner toolWithOwner)
+ toolWithOwner.SetToolOwner(stateToolOwnerType);
return res;
}
res = CreateInstance(type);
res.hideFlags = HideFlags.DontSave;
singletonObjects.Add(res);
- if (res is EditorToolContext context)
- context.SetContextOwner(stateToolOwnerType);
+ if (res is IHasToolOwner newToolWithOwner)
+ newToolWithOwner.SetToolOwner(stateToolOwnerType);
return res;
}
diff --git a/Editor/Mono/Tools/EditorToolSettingsOverlay.cs b/Editor/Mono/Tools/EditorToolSettingsOverlay.cs
index 0e5f7fb803..9421dc19c3 100644
--- a/Editor/Mono/Tools/EditorToolSettingsOverlay.cs
+++ b/Editor/Mono/Tools/EditorToolSettingsOverlay.cs
@@ -14,7 +14,7 @@
namespace UnityEditor.EditorTools
{
- [Overlay(typeof(ISupportsToolsOverlays), "Tool Settings", true, priority = (int)OverlayPriority.ToolSettings, defaultDockZone = DockZone.TopToolbar, defaultDockPosition = DockPosition.Top, defaultDockIndex = 0, group = OverlayAttribute.unityGroup)]
+ [Overlay(typeof(ISupportsEditorTools), "Tool Settings", true, priority = (int)OverlayPriority.ToolSettings, defaultDockZone = DockZone.TopToolbar, defaultDockPosition = DockPosition.Top, defaultDockIndex = 0, group = OverlayAttribute.unityGroup)]
[Icon("Icons/Overlays/ToolSettings.png")]
sealed class EditorToolSettingsOverlay : Overlay, ICreateToolbar, ICreateHorizontalToolbar, ICreateVerticalToolbar
{
diff --git a/Editor/Mono/Tools/EditorToolUtility.cs b/Editor/Mono/Tools/EditorToolUtility.cs
index 4f54495cbe..ea94b07e0c 100644
--- a/Editor/Mono/Tools/EditorToolUtility.cs
+++ b/Editor/Mono/Tools/EditorToolUtility.cs
@@ -4,18 +4,20 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using Unity.Collections;
using UnityEditor.Overlays;
+using UnityEditor.UIElements;
using UnityEngine;
-using UObject = UnityEngine.Object;
+using UnityEngine.UIElements;
namespace UnityEditor.EditorTools
{
- [EditorBrowsable(EditorBrowsableState.Never)]
- interface ISupportsToolsOverlays {}
+ public interface ISupportsEditorTools : ISupportsOverlays
+ {
+ public Camera handlesCamera { get; }
+ }
static class EditorToolUtility
{
@@ -866,6 +868,17 @@ internal static bool IsCustomToolContext(EditorToolContext context)
return context != null && context.GetType() != typeof(GameObjectToolContext);
}
+ internal static Type ResolveToolOwnerType(Type cachedType, string typeName)
+ {
+ if (cachedType == null && !String.IsNullOrEmpty(typeName))
+ cachedType = Type.GetType(typeName);
+
+ if (cachedType == null)
+ cachedType = typeof(SceneView);
+
+ return cachedType;
+ }
+
internal static void OrderAvailableTools(List tools)
{
tools.Sort((a, b) =>
@@ -905,5 +918,24 @@ internal static void OrderAvailableTools(List tools)
return a.GetHashCode().CompareTo(b.GetHashCode());
});
}
+
+ internal static VisualElement CreateEditorToolsIMGUIContainer(EditorWindow window, Action onGUIHandler)
+ {
+ var container = new IMGUIContainer()
+ {
+ onGUIHandler = onGUIHandler,
+ name = "EditorToolsIMGUIContainer",
+ pickingMode = PickingMode.Position,
+ viewDataKey = window.name,
+ renderHints = RenderHints.ClipWithScissors,
+ requireMeasureFunction = false
+ };
+
+ UIElementsEditorUtility.AddDefaultEditorStyleSheets(container);
+ container.style.overflow = Overflow.Hidden;
+ container.style.flexGrow = 1;
+
+ return container;
+ }
}
}
diff --git a/Editor/Mono/Tools/EditorToolWindowBase.cs b/Editor/Mono/Tools/EditorToolWindowBase.cs
deleted file mode 100644
index 1fd75c7691..0000000000
--- a/Editor/Mono/Tools/EditorToolWindowBase.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-// Unity C# reference source
-// Copyright (c) Unity Technologies. For terms of use, see
-// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
-
-using UnityEditor.Overlays;
-using UnityEngine;
-using UnityEditor.UIElements;
-using UnityEngine.UIElements;
-
-namespace UnityEditor.EditorTools
-{
- abstract class EditorToolWindowBase : EditorWindow, ISupportsOverlays, ISupportsToolsOverlays
- {
- public abstract Camera handlesCamera { get; }
-
- VisualElement m_ToolsIMGUIContainer;
-
- VisualElement toolsIMGUIContainer
- {
- get
- {
- if (m_ToolsIMGUIContainer == null)
- m_ToolsIMGUIContainer = CreateToolsIMGUIContainer();
-
- return m_ToolsIMGUIContainer;
- }
- }
-
- protected virtual void OnEnable()
- {
- rootVisualElement.Add(toolsIMGUIContainer);
- }
-
- void OnContainerGUI()
- {
- var toolsCamera = handlesCamera;
- if (toolsCamera != null)
- {
- var prevAspect = toolsCamera.aspect;
- var prevHandlesCamera = Handles.currentCamera;
- var prevCameraEnabled = toolsCamera.enabled;
- try
- {
- var containerSize = toolsIMGUIContainer.worldBound.size;
- toolsCamera.aspect = containerSize.y > 0f ? containerSize.x / containerSize.y : 1f;
- toolsCamera.enabled = true;
- Handles.SetCamera(toolsIMGUIContainer.worldBound, toolsCamera);
-
- EditorToolManager.OnToolGUI(this);
- }
- finally
- {
- toolsCamera.enabled = prevCameraEnabled;
- toolsCamera.aspect = prevAspect;
- Handles.currentCamera = prevHandlesCamera;
- }
- }
- }
-
- VisualElement CreateToolsIMGUIContainer()
- {
- var toolsIMGUIContainer = new IMGUIContainer()
- {
- onGUIHandler = OnContainerGUI,
- name = "EditorToolsWindowIMGUIContainer",
- pickingMode = PickingMode.Position,
- viewDataKey = name,
- renderHints = RenderHints.ClipWithScissors,
- requireMeasureFunction = false
- };
-
- UIElementsEditorUtility.AddDefaultEditorStyleSheets(toolsIMGUIContainer);
- toolsIMGUIContainer.style.overflow = Overflow.Hidden;
- toolsIMGUIContainer.style.flexGrow = 1;
-
- return toolsIMGUIContainer;
- }
- }
-}
diff --git a/Editor/Mono/Tools/PivotManager.cs b/Editor/Mono/Tools/PivotManager.cs
index 4f669bfded..cc30357b14 100644
--- a/Editor/Mono/Tools/PivotManager.cs
+++ b/Editor/Mono/Tools/PivotManager.cs
@@ -12,6 +12,8 @@ namespace UnityEditor
public abstract class CustomPivotMode : ScriptableObject
{
public abstract Vector3 position { get; }
+ public virtual void OnActivated() {}
+ public virtual void OnWillBeDeactivated() {}
}
[CustomPivot(k_DisplayName, tooltip = k_Tooltip, priority = CustomPivotAttribute.defaultPriority)]
@@ -60,6 +62,8 @@ public override Vector3 position
public abstract class CustomPivotRotation : ScriptableObject
{
public abstract Quaternion rotation { get; }
+ public virtual void OnActivated() {}
+ public virtual void OnWillBeDeactivated() {}
}
[CustomPivot(k_DisplayName, tooltip = k_Tooltip, priority = CustomPivotAttribute.defaultPriority)]
diff --git a/Editor/Mono/Tools/ToolManager.cs b/Editor/Mono/Tools/ToolManager.cs
index c4068ba48e..00385225c2 100644
--- a/Editor/Mono/Tools/ToolManager.cs
+++ b/Editor/Mono/Tools/ToolManager.cs
@@ -7,6 +7,7 @@
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEngine;
+using UnityEngine.Bindings;
using UObject = UnityEngine.Object;
namespace UnityEditor.EditorTools
@@ -54,11 +55,29 @@ public static void SetActiveContext() where T : EditorToolContext
{
SetActiveContext(typeof(T));
}
-
+
internal static void SetActiveContext(Type toolOwnerType) where T : EditorToolContext
{
SetActiveContext(typeof(T), toolOwnerType);
}
+
+ internal static bool CanSetActiveContext(Type context, Type contextOwner = null)
+ {
+ if (contextOwner == null)
+ contextOwner = typeof(SceneView);
+
+ if (context == null || !typeof(EditorToolContext).IsAssignableFrom(context) || context.IsAbstract)
+ return false;
+
+ if (!EditorToolUtility.IsComponentEditor(context))
+ return true;
+
+ return EditorToolManager.GetComponentContext(context, toolOwner: contextOwner, true) != null;
+ }
+
+ [VisibleToOtherModules("UnityEditor.UIToolkitAuthoringModule")]
+ internal static bool CanSetActiveContext() where T : EditorToolContext
+ => CanSetActiveContext(typeof(T));
internal static Type GetActiveToolType(Type toolOwnerType)
{
diff --git a/Editor/Mono/TypeCache.bindings.cs b/Editor/Mono/TypeCache.bindings.cs
index 11526630ff..8540099221 100644
--- a/Editor/Mono/TypeCache.bindings.cs
+++ b/Editor/Mono/TypeCache.bindings.cs
@@ -8,39 +8,67 @@
namespace UnityEditor
{
+ // The bindings generator derives the native names referenced by the generated
+ // [PreventExecutionInState] checks from this enum's name (g_TypeCachePreventExecutionBitField,
+ // TypeCachePreventExecutionChecks, TypeCachePreventExecution::ReportExecutionPrevention).
+ // Do not rename it independently of Runtime/Scripting/TypeCache.h, and keep the flag
+ // values in sync with TypeCachePreventExecutionChecks there.
+ internal enum TypeCachePreventExecution
+ {
+ kNoTypeCacheRestriction = 0,
+ kTypeCacheNotYetRefreshed = 1 << 0,
+ }
+
[NativeHeader("Runtime/Scripting/TypeCache.h")]
public static partial class TypeCache
{
+ const string k_TypeCacheNotYetRefreshedHowToFix = "Defer this query to a lifecycle callback such as [OnCodeInitializing] or [OnCodeLoaded].";
+
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesWithAttribute(Type attrType);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern MethodInfo[] Internal_GetMethodsWithAttribute(Type attrType);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern FieldInfo[] Internal_GetFieldsWithAttribute(Type attrType);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesDerivedFromInterface(Type interfaceType);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesDerivedFromType(Type parentType);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesWithAttributeFromAssembly(Type attrType, string assemblyName);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern MethodInfo[] Internal_GetMethodsWithAttributeFromAssembly(Type attrType, string assemblyName);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern FieldInfo[] Internal_GetFieldsWithAttributeFromAssembly(Type attrType, string assemblyName);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesDerivedFromInterfaceFromAssembly(Type interfaceType, string assemblyName);
[NativeMethod(IsThreadSafe = true)]
+ [PreventExecutionInState(TypeCachePreventExecution.kTypeCacheNotYetRefreshed, PreventExecutionSeverity.PreventExecution_ManagedException, k_TypeCacheNotYetRefreshedHowToFix)]
static extern Type[] Internal_GetTypesDerivedFromTypeFromAssembly(Type parentType, string assemblyName);
internal static extern ulong GetCurrentAge();
+
+ // Raises/lowers the kTypeCacheNotYetRefreshed restriction so tests can verify the
+ // generated [PreventExecutionInState] checks without having to run code inside the
+ // reload window, which is not reachable from user code.
+ internal static extern void Internal_SetPreventExecutionStateForTesting(bool restricted);
}
}
diff --git a/Editor/Mono/UIElements/Bindings/BindingStyleHelpers.cs b/Editor/Mono/UIElements/Bindings/BindingStyleHelpers.cs
index f585c09faf..a11e8b5969 100644
--- a/Editor/Mono/UIElements/Bindings/BindingStyleHelpers.cs
+++ b/Editor/Mono/UIElements/Bindings/BindingStyleHelpers.cs
@@ -29,6 +29,8 @@ enum DrivenPropertyState
static Action s_UpdateElementStyleFromProperty;
static Action s_UpdatePrefabStateStyleFromProperty;
+ const string k_ScrollTrackingHookedKey = "unity-prefab-override-scroll-tracked";
+
[VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.UIToolkitAuthoringModule")]
internal delegate void HandleRightClickMenuDelegate(VisualElement element, ref bool handled);
@@ -206,6 +208,7 @@ internal static void UpdateLivePropertyStyleFromProperty(VisualElement element,
// We intentionally re-register this event on the container per element and
// never unregister.
container.RegisterCallback(UpdatePrefabOverrideOrLivePropertyBarStyleEvent, BarType.LiveProperty);
+ RegisterBarScrollTracking(element, container);
element.RegisterCallback(_ =>
{
element.RemoveFromClassList(BindingExtensions.livePropertyUssClassName);
@@ -304,6 +307,7 @@ private static void UpdatePrefabStateStyleFromProperty(VisualElement element, Se
// We intentionally re-register this event on the container per element and
// never unregister.
container.RegisterCallback(UpdatePrefabOverrideOrLivePropertyBarStyleEvent, BarType.PrefabOverride);
+ RegisterBarScrollTracking(element, container);
element.RegisterCallback(_ =>
{
element.RemoveFromClassList(BindingExtensions.prefabOverrideUssClassName);
@@ -349,7 +353,8 @@ private static void UpdatePrefabOverrideOrLivePropertyBarStyle(VisualElement bar
return;
// Move the bar to where the control is in the container.
- var top = element.worldBound.y - container.worldBound.y;
+ var containerY = container.worldBound.y;
+ var top = element.worldBound.y - containerY;
if (float.IsNaN(top)) // If this is run before the container has been layed out.
return;
@@ -364,9 +369,10 @@ private static void UpdatePrefabOverrideOrLivePropertyBarStyle(VisualElement bar
if (elementHeight == 0f)
{
- bar.style.top = 0f;
- bar.style.height = 0f;
- bar.style.left = 0f;
+ CollapseBar(bar);
+ // The field can be transiently zero-height while a ListView recycles rows. Recover
+ // when its geometry resolves so the bar does not stay hidden.
+ element.RegisterCallback(ReUpdateLivePropertyBarStyleEvent, bar);
return;
}
@@ -376,11 +382,79 @@ private static void UpdatePrefabOverrideOrLivePropertyBarStyle(VisualElement bar
var bottomOffset = element.resolvedStyle.marginBottom;
var topOffset = element.resolvedStyle.marginTop;
- bar.style.top = top - topOffset;
- bar.style.height = elementHeight + bottomOffset + topOffset;
+ var barTop = top - topOffset;
+ var barBottom = barTop + elementHeight + bottomOffset + topOffset;
+
+ // Clip the bar to any scrolling ancestor (e.g. a ListView) between the field and
+ // the inspector so it follows the scroll and stops at the list's edges (UUM-142807).
+ for (var p = element.hierarchy.parent; p != null && p != container; p = p.hierarchy.parent)
+ {
+ if (p is ScrollView scrollView)
+ {
+ var viewport = scrollView.contentViewport.worldBound;
+ // Skip clipping until the viewport has a resolved size. A transient
+ // zero-height viewport (e.g. right after expanding a foldout) would otherwise
+ // collapse every bar until the next scroll.
+ if (float.IsNaN(viewport.y) || viewport.height <= 0f)
+ continue;
+
+ var viewportTop = viewport.y - containerY;
+ var viewportBottom = viewportTop + viewport.height;
+ barTop = Mathf.Max(barTop, viewportTop);
+ barBottom = Mathf.Min(barBottom, viewportBottom);
+ }
+ }
+
+ if (barBottom <= barTop)
+ {
+ CollapseBar(bar);
+ return;
+ }
+
+ bar.style.top = barTop;
+ bar.style.height = barBottom - barTop;
bar.style.left = 0.0f;
}
+ static void RegisterBarScrollTracking(VisualElement element, InspectorElement inspector)
+ {
+ for (var p = element.hierarchy.parent; p != null && p != inspector; p = p.hierarchy.parent)
+ {
+ if (p is ScrollView scrollView && !scrollView.HasProperty(k_ScrollTrackingHookedKey))
+ {
+ scrollView.SetProperty(k_ScrollTrackingHookedKey, null);
+
+ // Reposition on scroll (transform), and again after layout settles. The content
+ // container's geometry changes when the list virtualizes/recycles rows or when
+ // the foldout expands; the viewport's changes on resize. Repositioning on those
+ // post-layout events keeps the bars correct after a wheel notch recycles rows,
+ // which a scroll-only reposition would leave placed against transient geometry.
+ scrollView.verticalScroller.valueChanged += _ => RepositionAllBars(inspector);
+ scrollView.contentContainer.RegisterCallback(_ => RepositionAllBars(inspector));
+ scrollView.contentViewport.RegisterCallback(_ => RepositionAllBars(inspector));
+ }
+ }
+ }
+
+ static void RepositionAllBars(InspectorElement inspector)
+ {
+ RepositionBars(inspector.prefabOverrideBlueBarsContainer);
+ RepositionBars(inspector.livePropertyYellowBarsContainer);
+ }
+
+ static void RepositionBars(VisualElement barContainer)
+ {
+ for (var i = 0; i < barContainer.childCount; i++)
+ UpdatePrefabOverrideOrLivePropertyBarStyle(barContainer[i]);
+ }
+
+ static void CollapseBar(VisualElement bar)
+ {
+ bar.style.top = 0f;
+ bar.style.height = 0f;
+ bar.style.left = 0f;
+ }
+
private static void UpdatePrefabOverrideOrLivePropertyBarStyleEvent(GeometryChangedEvent evt, BarType barType)
{
var container = evt.target as InspectorElement;
diff --git a/Editor/Mono/UIElements/Bindings/BindingsInterface.cs b/Editor/Mono/UIElements/Bindings/BindingsInterface.cs
index a12e7a6f9b..3b5f381877 100644
--- a/Editor/Mono/UIElements/Bindings/BindingsInterface.cs
+++ b/Editor/Mono/UIElements/Bindings/BindingsInterface.cs
@@ -41,8 +41,11 @@ public static class BindingExtensions
internal static readonly string prefabOverrideBarUssClassName = "unity-binding__prefab-override-bar";
internal static readonly string prefabOverrideBarNotApplicableUssClassName = "unity-binding__prefab-override-bar-not-applicable";
internal static readonly UniqueStyleString drivenUssClassName = new("unity-binding--driven");
+ [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.UIToolkitAuthoringModule")]
internal static readonly UniqueStyleString animationAnimatedUssClassName = new("unity-binding--animation-animated");
+ [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.UIToolkitAuthoringModule")]
internal static readonly UniqueStyleString animationRecordedUssClassName = new("unity-binding--animation-recorded");
+ [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.UIToolkitAuthoringModule")]
internal static readonly UniqueStyleString animationCandidateUssClassName = new("unity-binding--animation-candidate");
internal static readonly UniqueStyleString livePropertyUssClassName = new("unity-binding--live-property");
internal static readonly string livePropertyBarName = "unity-binding-live-property-bar";
diff --git a/Editor/Mono/UIElements/Controls/BackgroundField.cs b/Editor/Mono/UIElements/Controls/BackgroundField.cs
index b29332959e..344315e110 100644
--- a/Editor/Mono/UIElements/Controls/BackgroundField.cs
+++ b/Editor/Mono/UIElements/Controls/BackgroundField.cs
@@ -42,6 +42,7 @@ public BackgroundField(string label) : base(label, null)
{
m_TypeOptions = new Dictionary();
m_ObjectField = new ObjectField().WithClassList(objectFieldUssClassName);
+ m_ObjectField.allowBuiltinResources = false;
m_ObjectField.RegisterValueChangedCallback(OnObjectValueChange);
m_ObjectField.objectFieldDisplay.RegisterDefaultDragAndDrop(new List() { typeof(Texture2D), typeof(RenderTexture), typeof(Sprite), typeof(VectorImage) });
diff --git a/Editor/Mono/UIElements/Controls/CategoryDropdownField/CategoryDropdownField.cs b/Editor/Mono/UIElements/Controls/CategoryDropdownField/CategoryDropdownField.cs
index 47b77b4e21..697e44a65d 100644
--- a/Editor/Mono/UIElements/Controls/CategoryDropdownField/CategoryDropdownField.cs
+++ b/Editor/Mono/UIElements/Controls/CategoryDropdownField/CategoryDropdownField.cs
@@ -123,7 +123,33 @@ void ShowMenu()
public override void SetValueWithoutNotify(string newValue)
{
base.SetValueWithoutNotify(newValue);
- ((INotifyValueChanged) m_Input).SetValueWithoutNotify(value);
+ ((INotifyValueChanged) m_Input).SetValueWithoutNotify(GetDisplayNameForValue(value));
+ }
+
+ string GetDisplayNameForValue(string itemValue)
+ {
+ if (TryGetDisplayName(ref recentCategoryContent, itemValue, out var displayName) ||
+ TryGetDisplayName(ref categoryContent, itemValue, out displayName))
+ return displayName;
+
+ return itemValue;
+ }
+
+ static bool TryGetDisplayName(ref CategoryDropdownContent content, string itemValue, out string displayName)
+ {
+ var items = content.Items;
+ for (var i = 0; i < items.Count; ++i)
+ {
+ var item = items[i];
+ if (item.itemType == CategoryDropdownContent.ItemType.Item && item.value == itemValue)
+ {
+ displayName = item.displayName;
+ return true;
+ }
+ }
+
+ displayName = null;
+ return false;
}
class PopupTextElement : TextElement
diff --git a/Editor/Mono/UIElements/Controls/ColorField.cs b/Editor/Mono/UIElements/Controls/ColorField.cs
index 499b7f25be..5824dd117e 100644
--- a/Editor/Mono/UIElements/Controls/ColorField.cs
+++ b/Editor/Mono/UIElements/Controls/ColorField.cs
@@ -14,7 +14,7 @@ namespace UnityEditor.UIElements
///
/// Makes a field for selecting a color. For more information, refer to [[wiki:UIE-uxml-element-ColorField|UXML element ColorField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ColorField.png")]
public partial class ColorField : BaseField
{
@@ -86,7 +86,7 @@ public bool hdr
}
}
- [VisibleToOtherModules("UnityEditor.UIBuilderModule")]
+ [VisibleToOtherModules("UnityEditor.UIBuilderModule", "UnityEditor.UIToolkitAuthoringModule")]
internal bool setAlphaIfTransparentWhenPicked;
bool m_ShowAlpha;
diff --git a/Editor/Mono/UIElements/Controls/CurveField.cs b/Editor/Mono/UIElements/Controls/CurveField.cs
index dbefc060bf..6c44f6014b 100644
--- a/Editor/Mono/UIElements/Controls/CurveField.cs
+++ b/Editor/Mono/UIElements/Controls/CurveField.cs
@@ -16,7 +16,7 @@ namespace UnityEditor.UIElements
///
/// Makes a field for editing an . For more information, refer to [[wiki:UIE-uxml-element-CurveField|UXML element CurveField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/CurveField.png")]
public partial class CurveField : BaseField
{
diff --git a/Editor/Mono/UIElements/Controls/EnumFlagsField.cs b/Editor/Mono/UIElements/Controls/EnumFlagsField.cs
index cc4edf93d0..4f23675678 100644
--- a/Editor/Mono/UIElements/Controls/EnumFlagsField.cs
+++ b/Editor/Mono/UIElements/Controls/EnumFlagsField.cs
@@ -23,7 +23,7 @@ namespace UnityEditor.UIElements
/// For more information, refer to [[wiki:UIE-uxml-element-EnumFlagsField|UXML element EnumFlagsField]].
///
[Icon("UIToolkit/Icons/EnumFlagsField.png")]
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class EnumFlagsField : BaseMaskField
{
///
diff --git a/Editor/Mono/UIElements/Controls/FontDefinitionField.cs b/Editor/Mono/UIElements/Controls/FontDefinitionField.cs
index 0ae2a50c88..2b05513817 100644
--- a/Editor/Mono/UIElements/Controls/FontDefinitionField.cs
+++ b/Editor/Mono/UIElements/Controls/FontDefinitionField.cs
@@ -53,6 +53,7 @@ public FontDefinitionField(string label) : base(label, null)
{
m_TypeOptions = new Dictionary();
m_ObjectField = new ObjectField().WithClassList(objectFieldUssClassName);
+ m_ObjectField.allowBuiltinResources = false;
m_ObjectField.RegisterValueChangedCallback(OnObjectValueChange);
m_ObjectField.objectFieldDisplay.RegisterDefaultDragAndDrop(new List() { typeof(Font), typeof(FontAsset) });
diff --git a/Editor/Mono/UIElements/Controls/GradientField.cs b/Editor/Mono/UIElements/Controls/GradientField.cs
index 2dd586523d..8e09ba701e 100644
--- a/Editor/Mono/UIElements/Controls/GradientField.cs
+++ b/Editor/Mono/UIElements/Controls/GradientField.cs
@@ -14,7 +14,7 @@ namespace UnityEditor.UIElements
///
/// Makes a field for editing an . For more information, refer to [[wiki:UIE-uxml-element-GradientField|UXML element GradientField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/GradientField.png")]
public partial class GradientField : BaseField
{
@@ -127,7 +127,7 @@ internal static Gradient GradientCopy(Gradient other)
VisualElement m_GradientTextureImage;
readonly Background m_DefaultBackground = new Background();
- bool isShowingGradientPicker => GradientPicker.visible && rawValue != null && ReferenceEquals(GradientPicker.gradient, rawValue);
+ internal bool isShowingGradientPicker => GradientPicker.visible && rawValue != null && ReferenceEquals(GradientPicker.gradient, rawValue);
///
/// Constructor.
@@ -223,7 +223,7 @@ void OnAttach()
UpdateGradientTexture();
}
- void ShowGradientPicker()
+ internal void ShowGradientPicker()
{
// Re-clicking the field while the picker is already shown for it must NOT advance the
// undo group or reset our bookkeeping. GradientPicker.Show() suppresses the previous
diff --git a/Editor/Mono/UIElements/Controls/LayerField.cs b/Editor/Mono/UIElements/Controls/LayerField.cs
index 1e633a6c08..1914a0ac7a 100644
--- a/Editor/Mono/UIElements/Controls/LayerField.cs
+++ b/Editor/Mono/UIElements/Controls/LayerField.cs
@@ -15,7 +15,7 @@ namespace UnityEditor.UIElements
/// A LayerField editor. For more information, refer to [[wiki:UIE-uxml-element-LayerField|UXML element LayerField]].
///
[Icon("UIToolkit/Icons/LayerField.png")]
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class LayerField : PopupField
{
[UxmlAttribute("value"), LayerDecorator]
diff --git a/Editor/Mono/UIElements/Controls/LayerMaskField.cs b/Editor/Mono/UIElements/Controls/LayerMaskField.cs
index eeef9459b4..cabc888bd8 100644
--- a/Editor/Mono/UIElements/Controls/LayerMaskField.cs
+++ b/Editor/Mono/UIElements/Controls/LayerMaskField.cs
@@ -14,7 +14,7 @@ namespace UnityEditor.UIElements
/// A LayerMaskField editor. For more information, refer to [[wiki:UIE-uxml-element-LayerMaskField|UXML element LayerMaskField]].
///
[Icon("UIToolkit/Icons/LayerMaskField.png")]
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class LayerMaskField : MaskField
{
[UxmlAttribute("choices"), UxmlAttributeBindingPath(nameof(choices)), HideInInspector]
diff --git a/Editor/Mono/UIElements/Controls/ObjectField.cs b/Editor/Mono/UIElements/Controls/ObjectField.cs
index 03069428d2..419823f761 100644
--- a/Editor/Mono/UIElements/Controls/ObjectField.cs
+++ b/Editor/Mono/UIElements/Controls/ObjectField.cs
@@ -16,7 +16,7 @@ namespace UnityEditor.UIElements
///
/// Makes a field to receive any object type. For more information, refer to [[wiki:UIE-uxml-element-ObjectField|UXML element ObjectField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ObjectField.png")]
public partial class ObjectField : BaseField
{
diff --git a/Editor/Mono/UIElements/Controls/PropertyField.cs b/Editor/Mono/UIElements/Controls/PropertyField.cs
index fa420355fc..8150e57c60 100644
--- a/Editor/Mono/UIElements/Controls/PropertyField.cs
+++ b/Editor/Mono/UIElements/Controls/PropertyField.cs
@@ -18,7 +18,7 @@ namespace UnityEditor.UIElements
/// A SerializedProperty wrapper VisualElement that, on [[BindingExtensions.Bind|Bind()]], will generate the correct field elements with the correct binding paths. For more information, refer to [[wiki:UIE-uxml-element-PropertyField|UXML element PropertyField]].
///
[Icon("UIToolkit/Icons/PropertyField.png")]
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class PropertyField : VisualElement, IBindable
{
static readonly BindingId labelProperty = nameof(label);
@@ -33,6 +33,7 @@ public partial class PropertyField : VisualElement, IBindable
static readonly string listViewNamePrefix = "unity-list-";
static readonly string buttonGroupNamePrefix = "unity-button-group-";
+ static readonly UniqueStyleString toggleButtonGroupFixedSizeUssClassNameUnique = new(ToggleButtonGroup.ussClassName + "--fixed-size");
///
/// Binding object that will be updated.
@@ -335,6 +336,14 @@ void ResetInternal(SerializedProperty newProperty)
else
{
RegisterPropertyChangesOnCustomDrawerElement(customPropertyGUI);
+
+ // Bind a custom drawer's root foldout to its expanded state (like the default drawer) so recursive expand/collapse reaches it.
+ if (customPropertyGUI is Foldout customFoldout
+ && string.IsNullOrEmpty(customFoldout.bindingPath)
+ && m_SerializedProperty.hasChildren)
+ {
+ customFoldout.bindingPath = m_SerializedProperty.propertyPath;
+ }
}
}
else
@@ -829,56 +838,71 @@ private VisualElement ConfigureLabelOnly(SerializedProperty property)
private static readonly Action SetListViewHeaderTitle = (view, s) => view.headerTitle = s;
private static readonly Action SetFoldoutText = (f, s) => f.text = s;
+ private EventCallback m_DebugAltKeyDownCallback;
+ private EventCallback m_DebugAltKeyUpCallback;
+
private void ConfigureDebugHelpers(TField field) where TField : BaseField
{
field.RegisterCallback>(AttachDebugCallbackOnPanel, (f, s) => f.label = s);
- field.RegisterCallback(DetachDebugCallbackFromPanel);
+ field.RegisterCallback(DetachDebugCallbackFromPanel);
}
private void ConfigureFoldoutDebugHelpers(Foldout view)
{
view.RegisterCallback>(AttachDebugCallbackOnPanel, SetFoldoutText);
- view.RegisterCallback(DetachDebugCallbackFromPanel);
+ view.RegisterCallback(DetachDebugCallbackFromPanel);
}
private void ConfigureListViewDebugHelpers(BaseListView view)
{
view.RegisterCallback>(AttachDebugCallbackOnPanel, SetListViewHeaderTitle);
- view.RegisterCallback(DetachDebugCallbackFromPanel);
+ view.RegisterCallback(DetachDebugCallbackFromPanel);
}
private void AttachDebugCallbackOnPanel(AttachToPanelEvent evt, Action setLabelAction)
- where T:VisualElement
+ where T : VisualElement
{
if (evt.destinationPanel == null)
return;
- var p = evt.destinationPanel;
+
+ var visualTree = evt.destinationPanel.visualTree;
var field = evt.elementTarget as T;
- var propertyPath = serializedProperty.propertyPath;
- var regularLabel = label ?? serializedProperty.localizedDisplayName;
- p.visualTree.RegisterCallback((e, f) =>
+
+ UnregisterDebugAltCallbacks(visualTree);
+
+ // A pooled list item can re-attach before being rebound, so read serializedProperty on key events and guard with isValid. (UUM-143676)
+ m_DebugAltKeyDownCallback = e =>
{
- if (e.altKey)
- {
- setLabelAction.Invoke(f, propertyPath);
- }
- }, field, TrickleDown.TrickleDown);
- p.visualTree.RegisterCallback((e, f) =>
+ if (e.altKey && serializedProperty.isValid)
+ setLabelAction.Invoke(field, serializedProperty.propertyPath);
+ };
+ m_DebugAltKeyUpCallback = e =>
{
if (!e.altKey)
- {
- setLabelAction.Invoke(f, regularLabel);
- }
- }, field, TrickleDown.TrickleDown);
+ setLabelAction.Invoke(field, label ?? (serializedProperty.isValid ? serializedProperty.localizedDisplayName : null));
+ };
+
+ visualTree.RegisterCallback(m_DebugAltKeyDownCallback, TrickleDown.TrickleDown);
+ visualTree.RegisterCallback(m_DebugAltKeyUpCallback, TrickleDown.TrickleDown);
}
- private void DetachDebugCallbackFromPanel(DetachFromPanelEvent evt)
- where T:VisualElement
+ private void DetachDebugCallbackFromPanel(DetachFromPanelEvent evt)
{
if (evt.originPanel != null)
+ UnregisterDebugAltCallbacks(evt.originPanel.visualTree);
+ }
+
+ private void UnregisterDebugAltCallbacks(VisualElement visualTree)
+ {
+ if (m_DebugAltKeyDownCallback != null)
{
- evt.originPanel.visualTree.UnregisterCallback>(AttachDebugCallbackOnPanel);
- evt.originPanel.visualTree.UnregisterCallback(DetachDebugCallbackFromPanel);
+ visualTree.UnregisterCallback(m_DebugAltKeyDownCallback, TrickleDown.TrickleDown);
+ m_DebugAltKeyDownCallback = null;
+ }
+ if (m_DebugAltKeyUpCallback != null)
+ {
+ visualTree.UnregisterCallback(m_DebugAltKeyUpCallback, TrickleDown.TrickleDown);
+ m_DebugAltKeyUpCallback = null;
}
}
@@ -931,17 +955,13 @@ VisualElement ConfigureListView(ListView listView, SerializedProperty property,
return listView;
}
- VisualElement ConfigureToggleButtonGroup(ToggleButtonGroup buttonGroup, SerializedProperty property, Func factory)
+ VisualElement ConfigureToggleButtonGroup(SerializedProperty property)
{
var propertyCopy = property.Copy();
- if (buttonGroup == null)
- {
- buttonGroup = factory();
- buttonGroup.AddToClassList(BaseField.alignedFieldUssClassName);
- buttonGroup.AddToClassList(ToggleButtonGroup.ussClassName + "--fixed-size");
- buttonGroup.RegisterValueChangedCallback(OnToggleGroupChanged);
- }
+ var buttonGroup = new ToggleButtonGroup();
+ buttonGroup.AddToClassList(BaseField.alignedFieldUssClassNameUnique);
+ buttonGroup.AddToClassList(toggleButtonGroupFixedSizeUssClassNameUnique);
var lengthProperty = propertyCopy.FindPropertyRelative("m_Length");
var dataProperty = propertyCopy.FindPropertyRelative("m_Data");
@@ -974,7 +994,15 @@ VisualElement ConfigureToggleButtonGroup(ToggleButtonGroup buttonGroup, Serializ
buttonGroup.SetProperty(BaseField.serializedPropertyCopyName, propertyCopy);
buttonGroup.name = buttonGroupName;
buttonGroup.label = fieldLabel;
- buttonGroup.Q(className: ToggleButtonGroup.buttonGroupClassName).Clear();
+
+ for (var i = 0; i < length; i++)
+ {
+ buttonGroup.Add(new Button { text = i.ToString() });
+ }
+
+ // Discard the adjustments populating made, then start reporting changes.
+ buttonGroup.SetValueWithoutNotify((ToggleButtonGroupState)propertyCopy.structValue);
+ buttonGroup.RegisterValueChangedCallback(OnToggleGroupChanged);
// Track changes to the ToggleButtonGroupState values.
buttonGroup.TrackPropertyValue(propertyCopy);
@@ -982,13 +1010,14 @@ VisualElement ConfigureToggleButtonGroup(ToggleButtonGroup buttonGroup, Serializ
buttonGroup.TrackPropertyValue(lengthProperty, OnPropertyChanged);
buttonGroup.TrackPropertyValue(dataProperty, OnPropertyChanged);
- for (var i = 0; i < length; i++)
- {
- buttonGroup.Add(new Button { text = i.ToString() });
- }
-
void OnToggleGroupChanged(ChangeEvent evt)
{
+ var current = (ToggleButtonGroupState)propertyCopy.structValue;
+
+ // Writing an unchanged value dirties the asset and rebuilds the inspector, which loops.
+ if (evt.newValue.Equals(current))
+ return;
+
propertyCopy.structValue = evt.newValue;
propertyCopy.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
@@ -1030,6 +1059,12 @@ void OnPropertyChanged(SerializedProperty _)
return buttonGroup;
}
+ // Named (not lambda) so -= matches on re-subscribe: the field is reused across rebinds and onValidateValue persists.
+ static uint ClampToUShort(uint v) => Math.Min(v, ushort.MaxValue);
+ static uint ClampToByte(uint v) => Math.Min(v, byte.MaxValue);
+ static int ClampToShort(int v) => Mathf.Clamp(v, short.MinValue, short.MaxValue);
+ static int ClampToSByte(int v) => Mathf.Clamp(v, sbyte.MinValue, sbyte.MaxValue);
+
private VisualElement CreateOrUpdateFieldFromProperty(SerializedProperty property, object originalField = null)
{
var propertyType = property.propertyType;
@@ -1060,23 +1095,23 @@ private VisualElement CreateOrUpdateFieldFromProperty(SerializedProperty propert
if (uintField != null)
{
+ // Remove both first: the field can be reused for another integer subtype, so drop any stale clamp.
+ uintField.onValidateValue -= ClampToUShort;
+ uintField.onValidateValue -= ClampToByte;
switch (property.type)
{
case "ushort":
- uintField.onValidateValue += v => Math.Min(v, ushort.MaxValue);
+ uintField.onValidateValue += ClampToUShort;
break;
case "byte":
- uintField.onValidateValue += v => Math.Min(v, byte.MaxValue);
+ uintField.onValidateValue += ClampToByte;
break;
- default:
- break;
-
}
-
+
}
return uintField;
}
-
+
{
var intField = ConfigureField(originalField as IntegerField, property,
() => new IntegerField()) as IntegerField;
@@ -1086,17 +1121,17 @@ private VisualElement CreateOrUpdateFieldFromProperty(SerializedProperty propert
// If the field was recycled from an ArraySize property
intField.isDelayed = false;
+ intField.onValidateValue -= ClampToShort;
+ intField.onValidateValue -= ClampToSByte;
switch (property.type)
{
case "short":
- intField.onValidateValue += v => Mathf.Clamp(v, short.MinValue, short.MaxValue);
+ intField.onValidateValue += ClampToShort;
break;
case "sbyte":
- intField.onValidateValue += v => Mathf.Clamp(v, sbyte.MinValue, sbyte.MaxValue);
+ intField.onValidateValue += ClampToSByte;
break;
- default:
- break;
- }
+ }
}
return intField;
@@ -1304,7 +1339,7 @@ private VisualElement CreateOrUpdateFieldFromProperty(SerializedProperty propert
case SerializedPropertyType.Generic:
if (property.type == nameof(ToggleButtonGroupState))
{
- return ConfigureToggleButtonGroup(originalField as ToggleButtonGroup, property, () => new ToggleButtonGroup());
+ return ConfigureToggleButtonGroup(property);
}
return property.isArray
diff --git a/Editor/Mono/UIElements/Controls/TagField.cs b/Editor/Mono/UIElements/Controls/TagField.cs
index 666e90c0da..ff88f6684d 100644
--- a/Editor/Mono/UIElements/Controls/TagField.cs
+++ b/Editor/Mono/UIElements/Controls/TagField.cs
@@ -15,7 +15,7 @@ namespace UnityEditor.UIElements
/// A editor. For more information, refer to [[wiki:UIE-uxml-element-TagField|UXML element TagField]].
///
[Icon("UIToolkit/Icons/TagField.png")]
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
public partial class TagField : PopupField
{
internal override string GetValueToDisplay()
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/Toolbar.cs b/Editor/Mono/UIElements/Controls/Toolbar/Toolbar.cs
index f611cdafcf..fb54e42c8e 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/Toolbar.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/Toolbar.cs
@@ -12,7 +12,7 @@ namespace UnityEditor.UIElements
///
/// A toolbar for tool windows. For more information, refer to [[wiki:UIE-uxml-element-Toolbar|UXML element Toolbar]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/Toolbar.png")]
public partial class Toolbar : VisualElement
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarBreadcrumbs.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarBreadcrumbs.cs
index ebf5f867ce..3bd6ded06e 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarBreadcrumbs.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarBreadcrumbs.cs
@@ -47,7 +47,7 @@ namespace UnityEditor.UIElements
/// ]]>
///
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarBreadcrumbs.png")]
public partial class ToolbarBreadcrumbs : VisualElement
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarButton.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarButton.cs
index 266e30f872..76e91e3257 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarButton.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarButton.cs
@@ -22,7 +22,7 @@ namespace UnityEditor.UIElements
///
/// SA: [[Button]], [[Toolbar]]
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarButton.png")]
public partial class ToolbarButton : Button
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarMenu.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarMenu.cs
index eedf40b2ed..07fd572f13 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarMenu.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarMenu.cs
@@ -13,7 +13,7 @@ namespace UnityEditor.UIElements
///
/// A drop-down menu for the toolbar. For more information, refer to [[wiki:UIE-uxml-element-ToolbarMenu|UXML element ToolbarMenu]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarMenu.png")]
public partial class ToolbarMenu : TextElement, IToolbarMenuElement
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarPopupSearchField.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarPopupSearchField.cs
index a4b8f7cd77..665decd57e 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarPopupSearchField.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarPopupSearchField.cs
@@ -13,7 +13,7 @@ namespace UnityEditor.UIElements
///
/// The pop-up search field for the toolbar. The search field includes a menu button. For more information, refer to [[wiki:UIE-uxml-element-ToolbarPopupSearchField|UXML element ToolbarPopupSearchField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarPopupSearchField.png")]
public partial class ToolbarPopupSearchField : ToolbarSearchField, IToolbarMenuElement
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSearchField.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSearchField.cs
index 58ff38ebbc..a3b541388b 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSearchField.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSearchField.cs
@@ -12,7 +12,7 @@ namespace UnityEditor.UIElements
///
/// A search field for the toolbar. For more information, refer to [[wiki:UIE-uxml-element-ToolbarSearchField|UXML element ToolbarSearchField]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarSearchField.png")]
public partial class ToolbarSearchField : SearchFieldBase
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSpacer.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSpacer.cs
index 0c4dd5251c..7ae6512b54 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSpacer.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarSpacer.cs
@@ -13,7 +13,7 @@ namespace UnityEditor.UIElements
///
/// A toolbar spacer of static size. For more information, refer to [[wiki:UIE-uxml-element-ToolbarSpacer|UXML element ToolbarSpacer]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarSpacer.png")]
public partial class ToolbarSpacer : VisualElement
{
diff --git a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarToggle.cs b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarToggle.cs
index afb9f4fa31..23e4d34733 100644
--- a/Editor/Mono/UIElements/Controls/Toolbar/ToolbarToggle.cs
+++ b/Editor/Mono/UIElements/Controls/Toolbar/ToolbarToggle.cs
@@ -12,7 +12,7 @@ namespace UnityEditor.UIElements
///
/// A toggle for the toolbar. For more information, refer to [[wiki:UIE-uxml-element-ToolbarToggle|UXML element ToolbarToggle]].
///
- [UxmlElement]
+ [UxmlElement(visibility = LibraryVisibility.Visible)]
[Icon("UIToolkit/Icons/ToolbarToggle.png")]
public partial class ToolbarToggle : Toggle
{
diff --git a/Editor/Mono/UIElements/Inspector/InspectorElement.cs b/Editor/Mono/UIElements/Inspector/InspectorElement.cs
index 52081dca7e..33442f6927 100644
--- a/Editor/Mono/UIElements/Inspector/InspectorElement.cs
+++ b/Editor/Mono/UIElements/Inspector/InspectorElement.cs
@@ -290,13 +290,13 @@ internal InspectorElement(Editor editor, DefaultInspectorFramework defaultInspec
prefabOverrideBlueBarsContainer = new VisualElement
{
name = BindingExtensions.prefabOverrideBarContainerName,
- style = { position = Position.Absolute }
+ pickingMode = PickingMode.Ignore
};
livePropertyYellowBarsContainer = new VisualElement
{
name = BindingExtensions.livePropertyBarContainerName,
- style = { position = Position.Absolute }
+ pickingMode = PickingMode.Ignore
};
Add(prefabOverrideBlueBarsContainer);
diff --git a/Editor/Mono/UIElements/StyleSheets/StyleSheetImporter.cs b/Editor/Mono/UIElements/StyleSheets/StyleSheetImporter.cs
index 099458ea1a..b4f8630fba 100644
--- a/Editor/Mono/UIElements/StyleSheets/StyleSheetImporter.cs
+++ b/Editor/Mono/UIElements/StyleSheets/StyleSheetImporter.cs
@@ -109,6 +109,8 @@ class StyleSheetImporterEditor : ScriptedImporterEditor
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_DisableValidation = serializedObject.FindProperty("disableValidation");
m_UnsupportedSelectorAction = serializedObject.FindProperty("unsupportedSelectorAction");
diff --git a/Editor/Mono/UIElements/StyleSheets/StyleSheetResourceUtil.cs b/Editor/Mono/UIElements/StyleSheets/StyleSheetResourceUtil.cs
index 737f12550a..4888b6719a 100644
--- a/Editor/Mono/UIElements/StyleSheets/StyleSheetResourceUtil.cs
+++ b/Editor/Mono/UIElements/StyleSheets/StyleSheetResourceUtil.cs
@@ -49,6 +49,7 @@ public static Object LoadResource(string pathName, System.Type type, float displ
if (type == typeof(Sprite))
{
// Special case for sprites, which are stored as Texture2D sub-assets
+ // Note: the path manipulation causes an allocation on every call
var spriteResource = Resources.Load(Path.GetFileNameWithoutExtension(pathName), type);
if (spriteResource != null)
resource = spriteResource;
diff --git a/Editor/Mono/UIElements/StyleSheets/ThemeStyleSheetImporterEditor.cs b/Editor/Mono/UIElements/StyleSheets/ThemeStyleSheetImporterEditor.cs
index cc2fc9809c..6b5a0fb05e 100644
--- a/Editor/Mono/UIElements/StyleSheets/ThemeStyleSheetImporterEditor.cs
+++ b/Editor/Mono/UIElements/StyleSheets/ThemeStyleSheetImporterEditor.cs
@@ -27,6 +27,8 @@ class ThemeStyleSheetImporterEditor : ScriptedImporterEditor
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_StyleSheetsProperty = extraDataSerializedObject.FindProperty("StyleSheets");
m_InheritedThemesProperty = extraDataSerializedObject.FindProperty("InheritedThemes");
diff --git a/Editor/Mono/UIElements/UIElementsEditorUtility.cs b/Editor/Mono/UIElements/UIElementsEditorUtility.cs
index 9b8c1da759..743bd2cd2b 100644
--- a/Editor/Mono/UIElements/UIElementsEditorUtility.cs
+++ b/Editor/Mono/UIElements/UIElementsEditorUtility.cs
@@ -187,7 +187,6 @@ internal static Action BindSerializedProperty(BaseField field, SerializedP
where T : struct
{
BindingsStyleHelpers.RegisterRightClickMenu(field, property);
- field.TrackPropertyValue(property);
field.AddToClassList(BaseField.alignedFieldUssClassName);
field.RegisterValueChangedCallback(e =>
@@ -195,12 +194,19 @@ internal static Action BindSerializedProperty(BaseField field, SerializedP
setter.Invoke(e.newValue, property);
});
+ // External-sync path. Must be non-notifying (SetValueWithoutNotify) so that refreshing the
+ // field from the property — e.g. on Reset/Undo via TrackPropertyValue, or manual polling by
+ // consumers like CameraEditor — does not raise the value-changed callback above, which would
+ // re-apply the property and push a new Undo step (wiping the Redo stack). See UUM-139005.
var updateCallback = () =>
{
- field.value = getter.Invoke(property);
+ var newValue = getter.Invoke(property);
+ if (!EqualityComparer.Default.Equals(field.value, newValue))
+ field.SetValueWithoutNotify(newValue);
field.schedule.Execute(() => BindingsStyleHelpers.UpdateElementStyle(field, property));
};
- updateCallback?.Invoke();
+ updateCallback.Invoke();
+ field.TrackPropertyValue(property, _ => updateCallback());
return updateCallback;
}
@@ -213,8 +219,6 @@ internal static Action BindSerializedProperty(DropdownField dropdown, Serialized
foreach (var val in stringValues)
dropdown.choices.Add(val.text);
- dropdown.TrackPropertyValue(property);
-
return BindSerializedProperty(dropdown, property, p =>
{
var value = boolProperty ? (property.boolValue ? 1 : 0) : property.intValue;
@@ -235,7 +239,6 @@ internal static Action BindSerializedProperty(DropdownField dropdown, Serialized
internal static Action BindSerializedProperty(DropdownField dropdown, SerializedProperty property, Func getter, Action setter)
{
BindingsStyleHelpers.RegisterRightClickMenu(dropdown, property);
- dropdown.TrackPropertyValue(property);
dropdown.AddToClassList(BaseField.alignedFieldUssClassName);
dropdown.RegisterValueChangedCallback(e =>
@@ -243,12 +246,16 @@ internal static Action BindSerializedProperty(DropdownField dropdown, Serialized
setter.Invoke(dropdown.index, property);
});
+ // External-sync path — see the note on the BaseField overload above (UUM-139005).
var updateCallback = () =>
{
- dropdown.index = getter.Invoke(property);
+ var newIndex = getter.Invoke(property);
+ if (dropdown.index != newIndex)
+ dropdown.SetIndexWithoutNotify(newIndex);
dropdown.schedule.Execute(() => BindingsStyleHelpers.UpdateElementStyle(dropdown, property));
};
- updateCallback?.Invoke();
+ updateCallback.Invoke();
+ dropdown.TrackPropertyValue(property, _ => updateCallback());
return updateCallback;
}
@@ -269,24 +276,31 @@ internal static Action BindSerializedProperty(EnumField enumField, Serialized
property.serializedObject.ApplyModifiedProperties();
onValueChange?.Invoke((T)e.newValue);
+ enumField.schedule.Execute(() => BindingsStyleHelpers.UpdateElementStyle(enumField, property));
});
+ // External-sync path. Uses SetValueWithoutNotify so refreshing from the property (Reset/Undo
+ // via TrackPropertyValue, or manual polling) doesn't raise the value-changed callback and
+ // re-apply the property — that would push a new Undo step and wipe the Redo stack (UUM-139005).
+ // Because the callback won't fire, onValueChange is invoked here so visibility/grouping side
+ // effects still run. The initialized flag fires onValueChange once for the initial setup, then
+ // only on genuine external changes — user-driven changes already fired it via the callback above.
+ bool initialized = false;
var updateCallback = () =>
{
- foreach (T value in Enum.GetValues(typeof(T)))
+ var propertyValue = boolProperty ? (property.boolValue ? 1 : 0) : property.intValue;
+ var value = (T)Enum.ToObject(typeof(T), propertyValue);
+ if (!initialized || !enumField.value.Equals(value))
{
- var propertyValue = boolProperty ? (property.boolValue ? 1 : 0) : property.intValue;
-
- if (value.ToInt32(null) != propertyValue)
- continue;
-
- enumField.value = value;
- break;
+ enumField.SetValueWithoutNotify(value);
+ onValueChange?.Invoke(value);
}
+ initialized = true;
enumField.schedule.Execute(() => BindingsStyleHelpers.UpdateElementStyle(enumField, property));
};
- updateCallback?.Invoke();
+ updateCallback.Invoke();
+ enumField.TrackPropertyValue(property, _ => updateCallback());
return updateCallback;
}
diff --git a/Editor/Mono/VersionControl/VCProvider.bindings.cs b/Editor/Mono/VersionControl/VCProvider.bindings.cs
index bdce34118f..36c7f68d77 100644
--- a/Editor/Mono/VersionControl/VCProvider.bindings.cs
+++ b/Editor/Mono/VersionControl/VCProvider.bindings.cs
@@ -47,6 +47,7 @@ private struct Traits
public bool enablesVersioningFolders;
public bool enablesChangelists;
public bool enablesLocking;
+ public bool enablesRevertUnchanged;
}
private static extern Traits activeTraits
@@ -75,6 +76,11 @@ public static bool hasLockingSupport
get { return activeTraits.enablesLocking; }
}
+ public static bool hasRevertUnchangedSupport
+ {
+ get { return activeTraits.enablesRevertUnchanged; }
+ }
+
public static bool isVersioningFolders
{
get { return activeTraits.enablesVersioningFolders; }
diff --git a/External/ScriptingCore/Unity.Scripting/AssemblyInfo.cs b/External/ScriptingCore/Unity.Scripting/AssemblyInfo.cs
index 4ed62f6128..c9a07e57ca 100644
--- a/External/ScriptingCore/Unity.Scripting/AssemblyInfo.cs
+++ b/External/ScriptingCore/Unity.Scripting/AssemblyInfo.cs
@@ -19,12 +19,14 @@
//Remove when we move API to be public
[assembly: InternalsVisibleTo("SomeTestAssembly")]
+[assembly: InternalsVisibleTo("UnityEngine.UnityAnalyticsModule")]
[assembly: InternalsVisibleTo("UnityEngine.ContentLoadModule")]
[assembly: InternalsVisibleTo("UnityEngine.HierarchyModule")]
[assembly: InternalsVisibleTo("UnityEditor.HierarchyModule")]
[assembly: InternalsVisibleTo("UnityEngine.HierarchyCoreModule")]
[assembly: InternalsVisibleTo("UnityEditor.ShaderFoundryModule")]
[assembly: InternalsVisibleTo("UnityEditor.QuickSearchModule")]
+[assembly: InternalsVisibleTo("UnityEditor.LightingModule")]
[assembly: InternalsVisibleTo("UnityEditor.EditorToolbarModule")]
[assembly: InternalsVisibleTo("UnityEditor.UIToolkitAuthoringModule")]
[assembly: InternalsVisibleTo("UnityEngine.InputModule")]
@@ -33,6 +35,14 @@
[assembly: InternalsVisibleTo("UnityEngine.InputForUIModule")]
[assembly: InternalsVisibleTo("UnityEngine.UIElementsModule")]
[assembly: InternalsVisibleTo("UnityEngine.IMGUIModule")]
+[assembly: InternalsVisibleTo("UnityEngine.MarshallingModule")]
+[assembly: InternalsVisibleTo("UnityEngine.AIModule")]
+[assembly: InternalsVisibleTo("UnityEditor.AIModule")]
+[assembly: InternalsVisibleTo("UnityEngine.VFXModule")]
+[assembly: InternalsVisibleTo("UnityEditor.VFXModule")]
+[assembly: InternalsVisibleTo("UnityEditor.ProjectAuditorModule")]
+[assembly: InternalsVisibleTo("UnityEngine.VideoModule")]
+[assembly: InternalsVisibleTo("UnityEditor.VideoModule")]
[assembly: InternalsVisibleTo("Assembly-CSharp-Editor-firstpass-testable")]
[assembly: InternalsVisibleTo("Assembly-CSharp-Editor-testable")]
[assembly: InternalsVisibleTo("Assembly-CSharp-testable")]
@@ -40,3 +50,5 @@
[assembly: InternalsVisibleTo("LifecycleTestAssembly2")]
[assembly: InternalsVisibleTo("LifecycleTestAssembly3")]
[assembly: InternalsVisibleTo("LifecycleTestAssembly4")]
+[assembly: InternalsVisibleTo("Unity.IntegrationTests.Scripting")]
+[assembly: InternalsVisibleTo("UnityEngine.AdaptivePerformanceModule")]
diff --git a/External/ScriptingCore/Unity.Scripting/CoreAttributes/PreserveAttribute.cs b/External/ScriptingCore/Unity.Scripting/CoreAttributes/PreserveAttribute.cs
index 6c4714865b..64b3b2db28 100644
--- a/External/ScriptingCore/Unity.Scripting/CoreAttributes/PreserveAttribute.cs
+++ b/External/ScriptingCore/Unity.Scripting/CoreAttributes/PreserveAttribute.cs
@@ -1,10 +1,7 @@
using System;
-namespace UnityEngine.Scripting
+namespace Unity.Private.Scripting;
+
+class PreserveAttribute : Attribute
{
- [System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property
- | AttributeTargets.Constructor | AttributeTargets.Interface | AttributeTargets.Delegate | AttributeTargets.Event | AttributeTargets.Struct | AttributeTargets.Assembly | AttributeTargets.Enum, Inherited = false)]
- public class PreserveAttribute : Attribute
- {
- }
}
diff --git a/External/ScriptingCore/Unity.Scripting/Diagnostics/Profiling.cs b/External/ScriptingCore/Unity.Scripting/Diagnostics/Profiling.cs
index 1072f6af16..781f154299 100644
--- a/External/ScriptingCore/Unity.Scripting/Diagnostics/Profiling.cs
+++ b/External/ScriptingCore/Unity.Scripting/Diagnostics/Profiling.cs
@@ -13,20 +13,26 @@ public readonly unsafe struct ProfilerCallbacks
{
public ProfilerCallbacks(
delegate* unmanaged[Cdecl] profiler_marker_create,
+ delegate* unmanaged[Cdecl] profiler_marker_get_static,
delegate* unmanaged[Cdecl] profiler_marker_begin_with_string,
delegate* unmanaged[Cdecl] profiler_marker_begin,
- delegate* unmanaged[Cdecl] profiler_marker_end)
+ delegate* unmanaged[Cdecl] profiler_marker_end,
+ delegate* unmanaged[Cdecl] profiler_domain_reload_phase = null)
{
this.profiler_marker_create = profiler_marker_create;
+ this.profiler_marker_get_static = profiler_marker_get_static;
this.profiler_marker_begin_with_string = profiler_marker_begin_with_string;
this.profiler_marker_begin = profiler_marker_begin;
this.profiler_marker_end = profiler_marker_end;
+ this.profiler_domain_reload_phase = profiler_domain_reload_phase;
}
public readonly delegate* unmanaged[Cdecl] profiler_marker_create;
+ public readonly delegate* unmanaged[Cdecl] profiler_marker_get_static;
public readonly delegate* unmanaged[Cdecl] profiler_marker_begin_with_string;
public readonly delegate* unmanaged[Cdecl] profiler_marker_begin;
public readonly delegate* unmanaged[Cdecl] profiler_marker_end;
+ public readonly delegate* unmanaged[Cdecl] profiler_domain_reload_phase;
}
///
@@ -39,6 +45,51 @@ public ProfilerCallbacks(
///
private static ProfilerCallbacks profilerCallbacks;
+ internal enum DomainReloadPhase
+ {
+ StartPhase1 = 1,
+ EndPhase1 = 2,
+ StartPhase2 = 3,
+ EndPhase2 = 4,
+ }
+
+ public readonly struct DomainReloadPhaseScope : IDisposable
+ {
+ private readonly DomainReloadPhase _endPhase;
+
+ internal DomainReloadPhaseScope(DomainReloadPhase startPhase, DomainReloadPhase endPhase)
+ {
+ _endPhase = endPhase;
+ EmitPhase(startPhase);
+ }
+
+ public void Dispose() => EmitPhase(_endPhase);
+ }
+
+ public static DomainReloadPhaseScope DomainReloadPhase1() =>
+ new(DomainReloadPhase.StartPhase1, DomainReloadPhase.EndPhase1);
+
+ public static DomainReloadPhaseScope DomainReloadPhase2() =>
+ new(DomainReloadPhase.StartPhase2, DomainReloadPhase.EndPhase2);
+
+ private static unsafe void EmitPhase(DomainReloadPhase phase)
+ {
+ if (profilerCallbacks.profiler_domain_reload_phase != null)
+ profilerCallbacks.profiler_domain_reload_phase((int)phase);
+ }
+
+ public static unsafe ProfilerMarker GetStaticMarker(string name)
+ {
+ if (profilerCallbacks.profiler_marker_get_static == null)
+ return default;
+
+ fixed (char* p = name)
+ {
+ IntPtr ptr = profilerCallbacks.profiler_marker_get_static(name.Length, p);
+ return new ProfilerMarker(ptr);
+ }
+ }
+
///
/// Struct that defines a code instrumentation scope.
///
@@ -55,6 +106,11 @@ public readonly unsafe struct ProfilerMarker
{
internal readonly IntPtr ptr;
+ internal ProfilerMarker(IntPtr existingPtr)
+ {
+ ptr = existingPtr;
+ }
+
public ProfilerMarker(string name)
{
if (profilerCallbacks.profiler_marker_create == null)
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ActiveLifecycleScopesTracker.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ActiveLifecycleScopesTracker.cs
index bd65e6e016..ca865c8b74 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ActiveLifecycleScopesTracker.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ActiveLifecycleScopesTracker.cs
@@ -435,14 +435,15 @@ private void TryEnterScope(LifecycleScopeWithContext lifecyc
activeScope.OnEnter(_scopeTransitionHelper);
}
- private void CollectNestedScopesToExit(string scopeName, List nestedActiveScopesInOrder)
+ private void CollectNestedScopesToExit(string scopeName, ref List? nestedActiveScopesInOrder)
{
foreach (var activeScope in _activeScopes.Values)
{
if (activeScope.Scope.MustBeNestedInsideScope(scopeName))
{
var activeScopeName = activeScope.Scope.Name;
- CollectNestedScopesToExit(activeScopeName, nestedActiveScopesInOrder);
+ CollectNestedScopesToExit(activeScopeName, ref nestedActiveScopesInOrder);
+ nestedActiveScopesInOrder ??= new List();
nestedActiveScopesInOrder.Add(activeScope);
}
}
@@ -456,7 +457,7 @@ private bool PrepareTryExitScope(LifecycleScope lifecycleScope, bool alsoExitNes
return false;
}
- var nestedActiveScopesInOrder = new List();
+ List? nestedActiveScopesInOrder = null;
int numActiveInstancesOfOuterScope = 0;
foreach (var activeScope in _activeScopes.Values)
@@ -469,10 +470,10 @@ private bool PrepareTryExitScope(LifecycleScope lifecycleScope, bool alsoExitNes
// We only exit nested scopes if we are exiting the last remaining active instance of the outer scope
if (numActiveInstancesOfOuterScope == 1)
{
- CollectNestedScopesToExit(lifecycleScope.Name, nestedActiveScopesInOrder);
+ CollectNestedScopesToExit(lifecycleScope.Name, ref nestedActiveScopesInOrder);
}
- if (nestedActiveScopesInOrder.Count > 0)
+ if (nestedActiveScopesInOrder != null)
{
if (!alsoExitNestedScopes)
{
@@ -486,7 +487,8 @@ private bool PrepareTryExitScope(LifecycleScope lifecycleScope, bool alsoExitNes
foreach (var nestedScope in nestedActiveScopesInOrder)
{
- DebugLifecycle.Log($"Lifecycle : Exiting nested scope '{nestedScope.Scope.Name}' due to exiting scope '{lifecycleScope.Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Exiting nested scope '{nestedScope.Scope.Name}' due to exiting scope '{lifecycleScope.Name}'");
nestedScope.OnExit(_scopeTransitionHelper);
RaiseAutoCleanups(nestedScope.Scope.GetType(), ScopeTransitionType.ExitScope);
_activeScopes.Remove(nestedScope.ScopeKey);
@@ -504,7 +506,7 @@ private bool PrepareTryExitScope(LifecycleScopeWithContext lifecycleScope,
return false;
}
- var nestedActiveScopesInOrder = new List();
+ List? nestedActiveScopesInOrder = null;
int numActiveInstancesOfOuterScope = 0;
foreach (var activeScope in _activeScopes.Values)
{
@@ -516,10 +518,10 @@ private bool PrepareTryExitScope(LifecycleScopeWithContext lifecycleScope,
// We only exit nested scopes if we are exiting the last remaining active instance of the outer scope
if (numActiveInstancesOfOuterScope == 1)
{
- CollectNestedScopesToExit(lifecycleScope.Name, nestedActiveScopesInOrder);
+ CollectNestedScopesToExit(lifecycleScope.Name, ref nestedActiveScopesInOrder);
}
- if (nestedActiveScopesInOrder.Count > 0)
+ if (nestedActiveScopesInOrder != null)
{
if (!alsoExitNestedScopes)
{
@@ -533,7 +535,8 @@ private bool PrepareTryExitScope(LifecycleScopeWithContext lifecycleScope,
foreach (var nestedScope in nestedActiveScopesInOrder)
{
- DebugLifecycle.Log($"Lifecycle : Exiting nested scope '{nestedScope.Scope.Name}' due to exiting scope '{lifecycleScope.Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Exiting nested scope '{nestedScope.Scope.Name}' due to exiting scope '{lifecycleScope.Name}'");
nestedScope.OnExit(_scopeTransitionHelper);
RaiseAutoCleanups(nestedScope.Scope.GetType(), ScopeTransitionType.ExitScope);
_activeScopes.Remove(nestedScope.ScopeKey);
@@ -597,7 +600,8 @@ private void CreateScopeTransitionRequest(LifecycleScope lifecycleScope, ScopeTr
_transitionRequestQueue.Enqueue(newRequest);
if (!transitionRequestQueueWasEmpty)
{
- DebugLifecycle.Log($"Lifecycle : Scope Transition request has been queued up for '{newRequest.Scope.Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Scope Transition request has been queued up for '{newRequest.Scope.Name}'");
// If there's older transition request in the queue, then we should process the new or other request right now, as they are being handled lower in the stack (or potentially in another thread).
return;
}
@@ -622,7 +626,8 @@ private void CreateScopeTransitionRequest(LifecycleScopeWithContext lifecy
_transitionRequestQueue.Enqueue(newRequest);
if (!transitionRequestQueueWasEmpty)
{
- DebugLifecycle.Log($"Lifecycle : Scope Transition request has been queued up for '{newRequest.Scope.Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Scope Transition request has been queued up for '{newRequest.Scope.Name}'");
// If there's older transition request in the queue, then we should process the new or other request right now, as they are being handled lower in the stack (or potentially in another thread).
return;
}
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/BurstScope.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/BurstScope.cs
index fd42c99af3..70fff059c1 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/BurstScope.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/BurstScope.cs
@@ -1,6 +1,6 @@
-using UnityEngine.Scripting;
using System.Reflection;
using System.Runtime.CompilerServices;
+using PreserveAttribute = Unity.Private.Scripting.PreserveAttribute;
[assembly: InternalsVisibleTo("UnityEditor.BurstModule")]
namespace Unity.Scripting.LifecycleManagement
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/CodeLoadedScope.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/CodeLoadedScope.cs
index d78604d6a7..4e9c4a65ff 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/CodeLoadedScope.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/CodeLoadedScope.cs
@@ -1,5 +1,5 @@
using System.Runtime.CompilerServices;
-using UnityEngine.Scripting;
+using PreserveAttribute = Unity.Private.Scripting.PreserveAttribute;
[assembly: InternalsVisibleTo("Unity.ScriptingTests.CodeLoadedGeneration")]
[assembly: InternalsVisibleTo("DomainReload-editor")]
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/DebugLifecycle.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/DebugLifecycle.cs
index 271af93f83..618b965d41 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/DebugLifecycle.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/DebugLifecycle.cs
@@ -11,6 +11,11 @@ static DebugLifecycle()
verificationEnabled = IsVerificationEnabled();
}
+ ///
+ /// Check before building log messages on hot paths to avoid interpolation allocations when logging is disabled.
+ ///
+ public static bool LoggingEnabled => loggingEnabled;
+
public static void Log(string message)
{
if (loggingEnabled)
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleController.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleController.cs
index 386bf56cc2..c5be25e299 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleController.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleController.cs
@@ -207,29 +207,29 @@ internal void ExpectPresentScope(string scopeName, LifecycleScopePresence expect
}
}
- private void ExecuteOnMainThread(string transitionType, string scopeName, Action action)
+ private bool CheckMainThread(string transitionType, string scopeName)
{
if (!IsOnMainThread)
{
DebugLifecycle.ReportError($"Lifecycle ERROR : {transitionType} scope {scopeName} can only be executed on the main thread\n" +
$"Calling thread was {Thread.CurrentThread.ManagedThreadId} while main thread is {MainThreadId}.");
- return;
+ return false;
}
- lock (_lock)
- {
- action.Invoke();
- }
+ return true;
}
internal void EnterScope()
where TScope : LifecycleScope, new()
{
var scope = new TScope();
- ExecuteOnMainThread("Enter", scope.Name, () =>
+ if (!CheckMainThread("Enter", scope.Name))
+ return;
+
+ lock (_lock)
{
_lifecycleTracker.RequestEnterScope(scope);
- });
+ }
}
// EnterScope() and ExitScope() functions have a void return value even though this scope transition request may fail.
@@ -239,26 +239,35 @@ internal void EnterScope()
// To improve on this we can return a small struct which contains info whether the request has been processed, whether it was succesful and the reason for failure in case it isn't
internal void EnterScope(LifecycleScope scope)
{
- ExecuteOnMainThread("Enter", scope.Name, () =>
+ if (!CheckMainThread("Enter", scope.Name))
+ return;
+
+ lock (_lock)
{
_lifecycleTracker.RequestEnterScope(scope);
- });
+ }
}
internal void EnterScope(LifecycleScopeWithContext scope)
where T : class
{
- ExecuteOnMainThread("Enter", scope.Name, () =>
+ if (!CheckMainThread("Enter", scope.Name))
+ return;
+
+ lock (_lock)
{
_lifecycleTracker.RequestEnterScope(scope);
- });
+ }
}
internal void ExitScope()
where TScope : LifecycleScope, new()
{
var scopeName = typeof(TScope).Name;
- ExecuteOnMainThread("Exit", scopeName, () =>
+ if (!CheckMainThread("Exit", scopeName))
+ return;
+
+ lock (_lock)
{
if (!_lifecycleTracker.TryGetActiveScope(out var scope))
{
@@ -268,7 +277,7 @@ internal void ExitScope()
_lifecycleTracker.RequestExitScope(scope!);
Debug.Assert(!_lifecycleTracker.IsOrWillBeInsideScope(scope));
- });
+ }
}
internal void ExitScope(TContext context)
@@ -276,7 +285,10 @@ internal void ExitScope(TContext context)
where TScope : LifecycleScopeWithContext
{
var scopeName = typeof(TScope).Name;
- ExecuteOnMainThread("Exit", scopeName, () =>
+ if (!CheckMainThread("Exit", scopeName))
+ return;
+
+ lock (_lock)
{
if (!_lifecycleTracker.TryGetActiveScope(context, out var scope))
{
@@ -286,24 +298,30 @@ internal void ExitScope(TContext context)
_lifecycleTracker.RequestExitScope(scope);
Debug.Assert(!_lifecycleTracker.IsOrWillBeInsideScopeWithActivationContext(scope.Context));
- });
+ }
}
internal void ExitScope(LifecycleScope scope)
{
- ExecuteOnMainThread("Exit", scope.Name, () =>
+ if (!CheckMainThread("Exit", scope.Name))
+ return;
+
+ lock (_lock)
{
_lifecycleTracker.RequestExitScope(scope);
- });
+ }
}
internal void ExitScope(LifecycleScopeWithContext scope)
where TContext : class
{
- ExecuteOnMainThread("Exit", scope.Name, () =>
+ if (!CheckMainThread("Exit", scope.Name))
+ return;
+
+ lock (_lock)
{
_lifecycleTracker.RequestExitScope(scope);
- });
+ }
}
internal void RegisterAutoCleanup(ClassAutoCleanup classAutoCleanup, Type scopeType, ScopeTransitionType cleanOn)
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleMethodRegistry.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleMethodRegistry.cs
index e9ef53a7d0..f2255f733f 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleMethodRegistry.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleMethodRegistry.cs
@@ -32,22 +32,28 @@ public void Register(Type lifecycleAttributeType, Assembly assembly, string meth
attributeTypes.Add(lifecycleAttributeType);
}
+ private static readonly List s_NoMethods = new();
+
internal List Get(Type lifecycleAttributeType, IReadOnlyList assemblies)
{
- var result = new List();
+ // Early-out without allocating: most attribute types have no registered methods at all
+ // and this runs on every scope transition (multiple times per domain reload).
+ if (!_lifecycleCallbacks.TryGetValue(lifecycleAttributeType, out var typeCallbacks) || typeCallbacks.Count == 0)
+ {
+ return s_NoMethods;
+ }
- if (_lifecycleCallbacks.TryGetValue(lifecycleAttributeType, out var typeCallbacks))
+ List? result = null;
+ foreach (var assembly in assemblies)
{
- foreach (var assembly in assemblies)
+ if (typeCallbacks.TryGetValue(assembly, out var assemblyCallbacks))
{
- if (typeCallbacks.TryGetValue(assembly, out var assemblyCallbacks))
- {
- result.AddRange(assemblyCallbacks);
- }
+ result ??= new List();
+ result.AddRange(assemblyCallbacks);
}
}
- return result;
+ return result ?? s_NoMethods;
}
internal void Clear(IReadOnlyList assemblies)
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleScopeBase.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleScopeBase.cs
index 5e3537d572..860472fdfa 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleScopeBase.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/LifecycleScopeBase.cs
@@ -43,16 +43,20 @@ public bool MustBeNestedInsideScope(string scopeName)
internal void OnEnter(ScopeTransitionHelper scopeTransitionHelper)
{
- DebugLifecycle.Log($"Lifecycle : Entering scope '{Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Entering scope '{Name}'");
Enter(scopeTransitionHelper);
- DebugLifecycle.Log($"Lifecycle : Entered scope '{Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Entered scope '{Name}'");
}
internal void OnExit(ScopeTransitionHelper scopeTransitionHelper)
{
- DebugLifecycle.Log($"Lifecycle : Exiting scope '{Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Exiting scope '{Name}'");
Exit(scopeTransitionHelper);
- DebugLifecycle.Log($"Lifecycle : Exited scope '{Name}'");
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : Exited scope '{Name}'");
}
}
}
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ManagedObjectsAwokenScope.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ManagedObjectsAwokenScope.cs
deleted file mode 100644
index a7296d1c83..0000000000
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ManagedObjectsAwokenScope.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("DomainReload-editor")]
-[assembly: InternalsVisibleTo("Assembly-CSharp-testable")]
-
-namespace Unity.Scripting.LifecycleManagement
-{
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- internal sealed class AfterManagedObjectsAwokenAttribute : LifecycleAttributeBase
- {
- }
-
- [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- internal sealed class BeforeManagedObjectsDisabledAttribute : LifecycleAttributeBase
- {
- }
- internal sealed class ManagedObjectsAwokenScope : LifecycleScope
- {
- public static readonly string ScopeName = "ManagedObjectsAwoken";
- public ManagedObjectsAwokenScope() : base(ScopeName)
- {
- ExplicitRequiredOuterScopes.Add(CodeInitializedScope.ScopeName);
- }
- protected override void Enter(ScopeTransitionHelper scopeTransitionHelper)
- {
- scopeTransitionHelper.ExecuteMethodsInOrder();
- }
-
- protected override void Exit(ScopeTransitionHelper scopeTransitionHelper)
- {
- scopeTransitionHelper.ExecuteMethodsInReverseOrder();
- }
- }
-}
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ScopeTransitionHelper.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ScopeTransitionHelper.cs
index f4f96c8035..ac53a9e752 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ScopeTransitionHelper.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/ScopeTransitionHelper.cs
@@ -17,6 +17,7 @@ internal sealed class ScopeTransitionHelper
private readonly StackOrderedAssemblyList _assemblyList = new();
private readonly LifecycleMethodRegistry _lifecycleMethodRegistry;
+ private readonly Dictionary _processMarkers = new();
internal INativeCallbackProvider? NativeCallbackProvider { get; set; }
@@ -35,6 +36,22 @@ private List FindStaticMethodsWithAttribute(Type attributeT
return _lifecycleMethodRegistry.Get(attributeType, assemblies);
}
+ private Profiling.ProfilerMarker GetProcessMarker(Type attributeType)
+ {
+ // Cache markers per attribute type: creating one allocates a string and calls into
+ // the native profiler, and this runs on every scope transition.
+ if (!_processMarkers.TryGetValue(attributeType, out var marker))
+ {
+ marker = new Profiling.ProfilerMarker(k_ProfilerMarkerPrefix + attributeType.Name);
+ if (marker.ptr != IntPtr.Zero)
+ {
+ _processMarkers.Add(attributeType, marker);
+ }
+ }
+
+ return marker;
+ }
+
///
/// Executes all static methods with the given attribute type in the given assemblies in the assembly order.
///
@@ -52,15 +69,16 @@ public void ExecuteMethodsInOrder(ReadOnlyAssemblyList? assemblies = null)
private void ExecuteMethodsInOrder(Type attributeType, IReadOnlyList assemblies)
{
- using var executeMethodsProfilerScope = new Profiling.ProfilerMarker(k_ProfilerMarkerPrefix + attributeType.Name).Auto();
-
var methods = FindStaticMethodsWithAttribute(attributeType, assemblies);
if (methods.Count == 0)
{
return;
}
- DebugLifecycle.Log($"Lifecycle : *inside scope transition* executing {methods.Count} hooks for type {attributeType}");
+ using var executeMethodsProfilerScope = GetProcessMarker(attributeType).Auto();
+
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : *inside scope transition* executing {methods.Count} hooks for type {attributeType}");
// Check if detailed profiling is enabled and create a marker which would wrap each method invocation
Profiling.ProfilerMarker? detailedInvokeMarker = EnableDetailedProfiling ? new Profiling.ProfilerMarker(k_DetailedInvokeMarkerPrefix + attributeType.Name) : null;
@@ -101,15 +119,16 @@ public void ExecuteMethodsInReverseOrder(ReadOnlyAssemblyList? assemblies = n
private void ExecuteMethodsInReverseOrder(Type attributeType, IReadOnlyList assemblies)
{
- using var executeMethodsProfilerScope = new Profiling.ProfilerMarker(k_ProfilerMarkerPrefix + attributeType.Name).Auto();
-
var methods = FindStaticMethodsWithAttribute(attributeType, assemblies);
if (methods.Count == 0)
{
return;
}
- DebugLifecycle.Log($"Lifecycle : *inside scope transition* executing {methods.Count} hooks for type {attributeType} in reverse");
+ using var executeMethodsProfilerScope = GetProcessMarker(attributeType).Auto();
+
+ if (DebugLifecycle.LoggingEnabled)
+ DebugLifecycle.Log($"Lifecycle : *inside scope transition* executing {methods.Count} hooks for type {attributeType} in reverse");
// Check if detailed profiling is enabled and create a marker which would wrap each method invocation
Profiling.ProfilerMarker? detailedInvokeMarker = EnableDetailedProfiling ? new Profiling.ProfilerMarker(k_DetailedInvokeMarkerPrefix + attributeType.Name) : null;
diff --git a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/UDMScope.cs b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/UDMScope.cs
index 29a44639e6..12e1cc24ec 100644
--- a/External/ScriptingCore/Unity.Scripting/LifecycleManagement/UDMScope.cs
+++ b/External/ScriptingCore/Unity.Scripting/LifecycleManagement/UDMScope.cs
@@ -1,4 +1,5 @@
using UnityEngine.Scripting;
+using PreserveAttribute = Unity.Private.Scripting.PreserveAttribute;
namespace Unity.Scripting.LifecycleManagement
{
diff --git a/External/ScriptingCore/Unity.Scripting/ReflectionFormatting.cs b/External/ScriptingCore/Unity.Scripting/ReflectionFormatting.cs
index c1ca4d8c1c..66ca02b811 100644
--- a/External/ScriptingCore/Unity.Scripting/ReflectionFormatting.cs
+++ b/External/ScriptingCore/Unity.Scripting/ReflectionFormatting.cs
@@ -9,6 +9,24 @@ namespace Unity.Scripting
*/
internal class ReflectionFormatting
{
- internal static string FormatMethod(MethodBase method) => $"{method.ReflectedType}.{method}";
+ internal static string FormatMethod(MethodBase method)
+ {
+ // MethodBase.ToString() is " ()". Drop the leading return type so
+ // the method portion matches Mono's mono_method_full_name, which omits it.
+ //
+ // Splitting on the first space is safe: a type rendered by reflection never contains a space
+ // (generic arguments are comma-separated with no space, e.g. "Dictionary`2[System.Int32,System.String]",
+ // arrays are "T[]"), so the first space always delimits the return type from the name. The
+ // ", " between parameters comes later and is preserved. We can't instead strip
+ // ReturnType.ToString() because that disagrees with the rendering here for some types (e.g.
+ // "System.Void" vs the "Void" produced above).
+ var signature = method.ToString();
+ int nameStart = signature.IndexOf(' ');
+ if (nameStart >= 0)
+ {
+ signature = signature.Substring(nameStart + 1);
+ }
+ return $"{method.ReflectedType}.{signature}";
+ }
}
}
diff --git a/Modules/AI/Builder/NavMeshBuilder.bindings.cs b/Modules/AI/Builder/NavMeshBuilder.bindings.cs
index 2e9cf560e7..4f58f2dfc4 100644
--- a/Modules/AI/Builder/NavMeshBuilder.bindings.cs
+++ b/Modules/AI/Builder/NavMeshBuilder.bindings.cs
@@ -8,10 +8,23 @@
namespace UnityEngine.AI
{
+ ///Navigation mesh builder interface.
[NativeHeader("Modules/AI/Builder/NavMeshBuilder.bindings.h")]
[StaticAccessor("NavMeshBuilderBindings", StaticAccessorType.DoubleColon)]
public static class NavMeshBuilder
{
+ ///Collects renderers or physics colliders, and terrains within a volume. This function might not collect some MeshColliders that are a distance greater than 1E7 from the origin.
+ ///For convenience, you can create a list of build sources directly from the current geometry.
+ ///
+ ///The collection can be controlled in terms of layers, type of geometry and by collecting either by hierarchy or volume.
+ /// The queried objects must overlap these bounds to be included in the results.
+ /// Specifies which layers are included in the query.
+ /// Which type of geometry to collect - e.g. physics colliders.
+ /// Area type to assign to results, unless modified by .
+ /// If true, all the source will be considered for generating links. Otherwise, only the marked sources will be considered.
+ /// List of markups which allows finer control over how objects are collected.
+ /// Specifies if only objects with markups are collected.
+ /// List where results are stored, the list is cleared at the beginning of the call.
public static void CollectSources(
Bounds includedWorldBounds, int includedLayerMask, NavMeshCollectGeometry geometry, int defaultArea, bool generateLinksByDefault,
List markups, bool includeOnlyMarkedObjects, List results)
@@ -30,6 +43,16 @@ public static void CollectSources(
results.AddRange(resultsArray);
}
+ ///Collects renderers or physics colliders, and terrains within a volume. This function might not collect some MeshColliders that are a distance greater than 1E7 from the origin.
+ ///For convenience, you can create a list of build sources directly from the current geometry.
+ ///
+ ///The collection can be controlled in terms of layers, type of geometry and by collecting either by hierarchy or volume.
+ /// The queried objects must overlap these bounds to be included in the results.
+ /// Specifies which layers are included in the query.
+ /// Which type of geometry to collect - e.g. physics colliders.
+ /// Area type to assign to results, unless modified by .
+ /// List of markups which allows finer control over how objects are collected.
+ /// List where results are stored, the list is cleared at the beginning of the call.
public static void CollectSources(
Bounds includedWorldBounds, int includedLayerMask, NavMeshCollectGeometry geometry, int defaultArea,
List markups, List results)
@@ -37,6 +60,18 @@ public static void CollectSources(
CollectSources(includedWorldBounds, includedLayerMask, geometry, defaultArea, false, markups, false, results);
}
+ ///Collects renderers or physics colliders, and terrains within a transform hierarchy.
+ ///For convenience, you can create a list of build sources directly from the current geometry.
+ ///
+ ///The collection can be controlled in terms of layers, type of geometry and by collecting either by hierarchy or volume.
+ /// If not null, consider only root and its children in the query; if null, includes everything loaded.
+ /// Specifies which layers are included in the query.
+ /// Which type of geometry to collect - e.g. physics colliders.
+ /// Area type to assign to results, unless modified by NavMeshMarkup.
+ /// If true, all the source will be considered for generating links. Otherwise, only the marked sources will be considered.
+ /// List of markups which allows finer control over how objects are collected.
+ /// Specifies if only objects with markups are collected.
+ /// List where results are stored, the list is cleared at the beginning of the call.
public static void CollectSources(
Transform root, int includedLayerMask, NavMeshCollectGeometry geometry, int defaultArea, bool generateLinksByDefault,
List markups, bool includeOnlyMarkedObjects, List results)
@@ -56,6 +91,16 @@ public static void CollectSources(
results.AddRange(resultsArray);
}
+ ///Collects renderers or physics colliders, and terrains within a transform hierarchy.
+ ///For convenience, you can create a list of build sources directly from the current geometry.
+ ///
+ ///The collection can be controlled in terms of layers, type of geometry and by collecting either by hierarchy or volume.
+ /// If not null, consider only root and its children in the query; if null, includes everything loaded.
+ /// Specifies which layers are included in the query.
+ /// Which type of geometry to collect - e.g. physics colliders.
+ /// Area type to assign to results, unless modified by NavMeshMarkup.
+ /// List of markups which allows finer control over how objects are collected.
+ /// List where results are stored, the list is cleared at the beginning of the call.
public static void CollectSources(
Transform root, int includedLayerMask, NavMeshCollectGeometry geometry, int defaultArea,
List markups, List results)
@@ -69,6 +114,15 @@ static extern NavMeshBuildSource[] CollectSourcesInternal(
NavMeshBuildMarkup[] markups, bool includeOnlyMarkedObjects);
// Immediate NavMeshData building
+ ///Builds a NavMesh data object from the provided input sources.
+ ///Note: that has same effect as creating a new empty and calling .
+ /// Settings for the bake process, see .
+ /// List of input geometry used for baking, they describe the surfaces to walk on or obstacles to avoid.
+ /// Bounding box relative to position and rotation which describes the volume where the NavMesh should be built. Empty bounds is treated as no bounds, i.e. the NavMesh will cover all the inputs.
+ /// Center of the NavMeshData. This specifies the origin for the NavMesh tiles.
+ /// Orientation of the NavMeshData, you can use this to generate NavMesh with an arbitrary up-vector – e.g. for walkable vertical surfaces.
+ ///The newly built NavMeshData, or null if the NavMeshData was empty or an error occurred.
+ ///
public static NavMeshData BuildNavMeshData(
NavMeshBuildSettings buildSettings, List sources,
Bounds localBounds, Vector3 position, Quaternion rotation)
@@ -87,6 +141,18 @@ public static NavMeshData BuildNavMeshData(
}
// Immediate NavMeshData updating
+ ///Incrementally updates the NavMeshData based on the sources.
+ ///Each time NavMeshData is built or updated, the source data is hashed, and the hashes are stored along with the .
+ ///
+ ///When called, first the hashes are recomputed and compared and only changed portions are rebuilt. For this reason, the list of sources should always contain all the input geometry, even if they haven't moved or changed. If the list of sources is modified between calls to UpdateNavMeshData the missing/added sources are considered changes. Try to provide the sources that have not changed since the last update in the same relative order as before because their sequence can affect the values of the hashes. This measure ensures that unchanged portions don't get rebuilt unnecessarily.
+ ///
+ ///You must supply a Bounds struct for the localBounds parameter.
+ /// The NavMeshData to update.
+ /// The build settings which is used to update the NavMeshData. The build settings is also hashed along with the data, so changing settings will cause a full rebuild.
+ /// List of input geometry used for baking, they describe the surfaces to walk on or obstacles to avoid.
+ /// Bounding box relative to position and rotation which describes the volume where the NavMesh should be built.
+ ///true if the update was successful.
+ ///
public static bool UpdateNavMeshData(
NavMeshData data, NavMeshBuildSettings buildSettings, List sources, Bounds localBounds)
{
@@ -102,6 +168,19 @@ static extern bool UpdateNavMeshDataListInternal(
NavMeshData data, NavMeshBuildSettings buildSettings, ReadOnlySpan sources, Bounds localBounds);
// Async NavMeshData updating
+ ///Asynchronously and incrementally updates the NavMeshData based on the sources.
+ ///Each time NavMeshData is built or updated, the source data is hashed, and the hashes are stored along with the NavMeshData.
+ ///
+ ///
+ ///When UpdateNavMeshDataAsync() is called, first the hashes are compared and only changed portions are rebuilt. For this reason, the list of sources should always contain all the input geometry, even if they haven't moved or changed. If the list of sources is modified between calls to UpdateNavMeshDataAsync the missing/added sources are considered changes. Try to provide the sources that have not changed since the last update in the same relative order as before because their sequence can affect the values of the hashes. This measure ensures that unchanged portions don't get rebuilt unnecessarily.
+ ///
+ ///You must supply a Bounds struct for the localBounds parameter.
+ /// The NavMeshData to update.
+ /// The build settings used to update the NavMeshData. The build settings are also hashed along with the data, so changing the settings is likely to cause a full rebuild.
+ /// List of input geometry used for baking, they describe the surfaces to walk on or obstacles to avoid.
+ /// Bounding box relative to position and rotation which describes to volume where the NavMesh should be built.
+ ///Can be used to check the progress of the update.
+ ///
public static AsyncOperation UpdateNavMeshDataAsync(
NavMeshData data, NavMeshBuildSettings buildSettings, List sources, Bounds localBounds)
{
@@ -113,6 +192,9 @@ public static AsyncOperation UpdateNavMeshDataAsync(
return UpdateNavMeshDataAsyncListInternal(data, buildSettings, NoAllocHelpers.CreateReadOnlySpan(sources), localBounds);
}
+ ///Cancels an asynchronous update of the specified NavMesh data.
+ /// The data associated with asynchronous updating.
+ ///
[NativeHeader("Modules/AI/NavMeshManager.h")]
[StaticAccessor("GetNavMeshManager().GetNavMeshBuildManager()", StaticAccessorType.Arrow)]
[NativeMethod("Purge")]
diff --git a/Modules/AI/Components/NavMeshAgent.bindings.cs b/Modules/AI/Components/NavMeshAgent.bindings.cs
index ede51532a4..9d4e187a92 100644
--- a/Modules/AI/Components/NavMeshAgent.bindings.cs
+++ b/Modules/AI/Components/NavMeshAgent.bindings.cs
@@ -9,139 +9,418 @@
namespace UnityEngine.AI
{
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Level of obstacle avoidance.
[MovedFrom("UnityEngine")]
public enum ObstacleAvoidanceType
{
- // Disable avoidance.
+ ///Disable avoidance.
NoObstacleAvoidance = 0,
- // Enable simple avoidance. Low performance impact.
+ ///Enable simple avoidance. Low performance impact.
LowQualityObstacleAvoidance = 1,
- // Medium avoidance. Medium performance impact
+ ///Medium avoidance. Medium performance impact.
MedQualityObstacleAvoidance = 2,
- // Good avoidance. High performance impact
+ ///Good avoidance. High performance impact.
GoodQualityObstacleAvoidance = 3,
- // Enable highest precision. Highest performance impact.
+ ///Enable highest precision. Highest performance impact.
HighQualityObstacleAvoidance = 4
}
- // Navigation mesh agent.
+ ///Navigation mesh agent.
+ ///Attach this component to a mobile character in the game to allow the character to use the NavMesh to navigate the scene. For more details refer to AI Navigation .
[MovedFrom("UnityEngine")]
[NativeHeader("Modules/AI/Components/NavMeshAgent.bindings.h")]
[NativeHeader("Modules/AI/NavMesh/NavMesh.bindings.h")]
[HelpURL("https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/NavMeshAgent.html")]
public sealed class NavMeshAgent : Behaviour
{
- // Sets or updates the destination. This triggers calculation for a new path.
+ ///Sets or updates the destination thus triggering the calculation for a new path.
+ ///Note that the path may not become available until after a few frames later.
+ ///While the path is being computed, will be true.
+ ///If a valid path becomes available then the agent will resume movement.
+ /// The target point to navigate to.
+ ///True if the destination was requested successfully, otherwise false.
+ ///
+ /// ();
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// if (Input.GetMouseButtonDown(0))
+ /// {
+ /// SetDestinationToMousePosition();
+ /// }
+ /// }
+ ///
+ /// void SetDestinationToMousePosition()
+ /// {
+ /// RaycastHit hit;
+ /// Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
+ /// if (Physics.Raycast(ray, out hit))
+ /// {
+ /// myNavMeshAgent.SetDestination(hit.point);
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
public extern bool SetDestination(Vector3 target);
- // Destination to navigate towards.
+ ///Gets or attempts to set the destination of the agent in world-space units.
+ ///Getting:
+ ///
+ ///Returns the destination set for this agent.
+ ///
+ ///• If a destination is set but the path is not yet processed the position returned will be valid navmesh position that's closest to the previously set position.<br>
+ ///• If the agent has no path or requested path - returns the agents position on the navmesh.<br>
+ ///• If the agent is not mapped to the navmesh (e.g. Scene has no navmesh) - returns a position at infinity.
+ ///
+ ///Setting:
+ ///
+ ///Requests the agent to move to the valid navmesh position that's closest to the requested destination.
+ ///
+ ///• The path result may not become available until after a few frames. Use to query for outstanding results.<br>
+ ///• If it's not possible to find a valid nearby navmesh position (e.g. Scene has no navmesh) no path is requested. Use and check return value if you need to handle this case explicitly.
+ ///
+ /// ();
+ /// destination = agent.destination;
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// // Update destination if the target moves one unit
+ /// if (Vector3.Distance(destination, target.position) > 1.0f)
+ /// {
+ /// destination = target.position;
+ /// agent.destination = destination;
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
public extern Vector3 destination { get; set; }
- // Stop within this distance from the target position.
+ ///Stop within this distance from the target position.
+ ///It is seldom possible to land exactly at the target point, so this property can be used to set an acceptable radius within which the agent should stop. A larger stopping distance will give the agent more room for manoeuvre at the end of the path and might avoid sudden braking, turning or other unconvincing AI behaviour.
public extern float stoppingDistance { get; set; }
- // The current velocity of the [[NavMeshAgent]] component.
+ ///Access the current velocity of the component, or set a velocity to control the agent manually.
+ ///Reading the variable will return the current velocity of the agent based on the crowd simulation.
+ ///
+ ///Setting the variable will override the simulation (including: moving towards destination, collision avoidance, and acceleration control) and command the NavMesh Agent to move using the specific velocity directly. When the agent is controlled using a velocity, its movement is still constrained on the NavMesh.
+ ///
+ ///Setting the velocity directly, can be used for implementing player characters, which are moving on NavMesh and affecting the rest of the simulated crowd. In addition, setting priority to high (a small value is higher priority), will make other simulated agents to avoid the player controlled agent even more eagerly.
+ ///
+ ///It is recommended to set the velocity each frame when controlling the agent manually, and if releasing the control to the simulation, set the velocity to zero. If agent’s velocity is set to some value and then stopped updating it, the simulation will pick up from there and the agent will slowly decelerate (assuming no destination is set).
+ ///
+ ///Note that reading the velocity will always return value from the simulation. If you set the value, the effect will show up in the next update. Since the returned velocity comes from the simulation (including avoidance and collision handling), it can be different than the one you set.
+ ///
+ ///The velocity is specified in distance units per second (same as physics), and represented in global coordinate system.
public extern Vector3 velocity { get; set; }
- // The next position on the path.
+ ///Gets or sets the simulation position of the navmesh agent.
+ ///
+ /// The position vector is in world space coordinates and units.
+ ///
+ ///The nextPosition is coupled to . In the default case the navmesh agent's Transform position will match the internal simulation position at the time the script Update function is called. This coupling can be turned on and off by setting .
+ ///
+ ///When is true, the reflects the simulated position, when false the position of the transform and the navmesh agent is not synchronized, and you'll see a difference between the two in general. When is turned back on, the will be immediately move to match nextPosition.
+ ///
+ ///By setting nextPosition you can directly control where the internal agent position should be. The agent will be moved towards the position, but is constrained by the navmesh connectivity and boundaries. As such it will be useful only if the positions are continuously updated and assessed.
+ ///
+ /// Additionally it can be useful to control the agent position directly - especially if the
+ ///gameobject transform is controlled by something else - e.g. animator, physics, scripted or input.
+ ///
+ ///
+ /// ().updatePosition = false;
+ /// }
+ ///
+ /// void OnAnimatorMove()
+ /// {
+ /// transform.position = GetComponent().nextPosition;
+ /// }
+ ///}
+ ///]]>
+ ///
+ ///
+ /// ();
+ /// agent.updatePosition = !agentIsControlledByOther;
+ /// if (agentIsControlledByOther)
+ /// {
+ /// GetComponent().nextPosition = transform.position;
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
+ ///
+ ///
[NativeProperty("Position")]
public extern Vector3 nextPosition { get; set; }
- // The current steering target - usually the next corner or end point of the current path. (RO)
+ ///Get the current steering target along the path.
+ ///This is typically the next corner along the path or the end point of the path.
+ ///
+ ///Unless the agent is moving on an , there is a straight path between the agent and the steeringTarget.
+ ///
+ ///When approaching an OffMeshLink for traversal - the value is the position where the agent will enter the link.
+ ///While agent is traversing an OffMeshLink the value is the position where the agent will leave the link.
public extern Vector3 steeringTarget { get; }
- // The desired velocity of the agent including any potential contribution from avoidance. (RO)
+ ///The desired velocity of the agent including any potential contribution from avoidance.
public extern Vector3 desiredVelocity { get; }
- // Remaining distance along the current path - or infinity when not known. (RO)
+ ///The distance between the agent's position and the destination on the current path.
+ ///If the remaining distance is unknown then this will have a value of infinity.
public extern float remainingDistance { get; }
- // The relative vertical displacement of the owning [[GameObject]].
+ ///The relative vertical displacement of the owning .
public extern float baseOffset { get; set; }
- // Is agent currently positioned on an OffMeshLink. (RO)
+ ///Is the agent currently positioned on an OffMeshLink?
+ ///This property is useful when is false and custom movement is needed when crossing the link.
+ ///
+ ///
public extern bool isOnOffMeshLink
{
[NativeName("IsOnOffMeshLink")]
get;
}
- // Enables or disables the current link.
+ ///Enables or disables the current off-mesh link.
+ ///This function activates or deactivates the off-mesh link
+ ///where the agent is currently waiting. This is useful for
+ ///granting access to newly discovered areas of the game world or
+ ///simulating the creation or removal of an obstacle to an area.
+ /// Is the link activated?
+ ///
+ /// ();
+ /// }
+ /// void OpenDiscoveredArea(Hashtable areasDiscovered) {
+ /// if (agent.isOnOffMeshLink)
+ /// if (areasDiscovered.ContainsKey(agent.currentOffMeshLinkData.offMeshLink.name))
+ /// agent.ActivateCurrentOffMeshLink(true);
+ /// }
+ ///}
+ ///]]>
+ ///
public extern void ActivateCurrentOffMeshLink(bool activated);
- // The current [[OffMeshLinkData]].
+ ///The current .
+ ///In the case that this agent is not on an OffMeshLink the is marked as invalid. See also
public OffMeshLinkData currentOffMeshLinkData => GetCurrentOffMeshLinkDataInternal();
[FreeFunction("NavMeshAgentScriptBindings::GetCurrentOffMeshLinkDataInternal", HasExplicitThis = true)]
internal extern OffMeshLinkData GetCurrentOffMeshLinkDataInternal();
- // The next [[OffMeshLinkData]] on the current path.
+ ///The next on the current path.
+ ///In the case that the current path does not contain an OffMeshLink the is marked as invalid.
public OffMeshLinkData nextOffMeshLinkData => GetNextOffMeshLinkDataInternal();
[FreeFunction("NavMeshAgentScriptBindings::GetNextOffMeshLinkDataInternal", HasExplicitThis = true)]
internal extern OffMeshLinkData GetNextOffMeshLinkDataInternal();
- // Terminate OffMeshLink occupation and transfer the agent to the closest point on other side.
+ ///Completes the movement on the current OffMeshLink.
+ ///The agent will move to the closest valid navmesh position on the other end of the current OffMeshLink.
+ ///
+ ///CompleteOffMeshLink has no effect unless the agent is on an OffMeshLink ().
+ ///
+ ///When is disabled an agent will pause at an off-mesh link until this function is called.
+ ///It is useful for implementing custom movement across OffMeshLinks.
+ ///
+ /// ();
+ /// agent.autoTraverseOffMeshLink = false;
+ /// while (true)
+ /// {
+ /// if (agent.isOnOffMeshLink)
+ /// {
+ /// if (method == OffMeshLinkMoveMethod.NormalSpeed)
+ /// yield return StartCoroutine(NormalSpeed(agent));
+ /// else if (method == OffMeshLinkMoveMethod.Parabola)
+ /// yield return StartCoroutine(Parabola(agent, 2.0f, 0.5f));
+ /// agent.CompleteOffMeshLink();
+ /// }
+ /// yield return null;
+ /// }
+ /// }
+ ///
+ /// IEnumerator NormalSpeed(NavMeshAgent agent)
+ /// {
+ /// OffMeshLinkData data = agent.currentOffMeshLinkData;
+ /// Vector3 endPos = data.endPos + Vector3.up * agent.baseOffset;
+ /// while (agent.transform.position != endPos)
+ /// {
+ /// agent.transform.position = Vector3.MoveTowards(agent.transform.position, endPos, agent.speed * Time.deltaTime);
+ /// yield return null;
+ /// }
+ /// }
+ ///
+ /// IEnumerator Parabola(NavMeshAgent agent, float height, float duration)
+ /// {
+ /// OffMeshLinkData data = agent.currentOffMeshLinkData;
+ /// Vector3 startPos = agent.transform.position;
+ /// Vector3 endPos = data.endPos + Vector3.up * agent.baseOffset;
+ /// float normalizedTime = 0.0f;
+ /// while (normalizedTime < 1.0f)
+ /// {
+ /// float yOffset = height * 4.0f * (normalizedTime - normalizedTime * normalizedTime);
+ /// agent.transform.position = Vector3.Lerp(startPos, endPos, normalizedTime) + yOffset * Vector3.up;
+ /// normalizedTime += Time.deltaTime / duration;
+ /// yield return null;
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
+ ///
public extern void CompleteOffMeshLink();
- // Automate movement onto and off of OffMeshLinks.
+ ///Should the agent move across OffMeshLinks automatically?
+ ///Off-mesh links are used to connect disjoint regions of the NavMesh. Usually, a character should be able to pass through or traverse a link automatically, which will happen if this property is set to true. However, it can also be set to false in cases where special control over movement is needed.
+ ///
+ ///
public extern bool autoTraverseOffMeshLink { get; set; }
- // Automate braking of NavMeshAgent to avoid overshooting the destination.
+ ///Should the agent brake automatically to avoid overshooting the destination point?
+ ///If the agent needs to land close to the destination point then it will typically need to brake to avoid overshooting or endless "orbiting" around the target zone. If this property is set to true, the agent will brake automatically as it nears the destination.
public extern bool autoBraking { get; set; }
- // Attempt to acquire a new path if the existing path becomes invalid
+ ///Should the agent attempt to acquire a new path if the existing path becomes invalid?
+ ///A new path calculation is also attempted aquired if the agent reaches the end of a partial and stale path.
public extern bool autoRepath { get; set; }
- // Does this agent currently have a path. (RO)
+ ///Does the agent currently have a path? (RO)
+ ///This property will be true if the agent has a path calculated to the desired destination and false otherwise.
public extern bool hasPath
{
[NativeName("HasPath")]
get;
}
- // A path is being computed, but not yet ready. (RO)
+ ///Is a path in the process of being computed but not yet ready? (RO)
public extern bool pathPending
{
[NativeName("PathPending")]
get;
}
- // Is the current path stale. (RO)
+ ///Is the current path stale. (RO)
+ ///When true, the path may no longer be valid or optimal.
+ ///This flag will be set if: there are any changes to the , if any is enabled or disabled, or if the costs for the NavMeshAreas have been changed.
public extern bool isPathStale
{
[NativeName("IsPathStale")]
get;
}
- // Query the state of the current path.
+ ///The status of the current path (complete, partial or invalid).
+ ///Returns if either the path is invalid, or the agent is not yet initialized. (.)
+ ///
public extern NavMeshPathStatus pathStatus { get; }
- //*undocumented*
+ ///
[NativeProperty("EndPositionOfCurrentPath")]
public extern Vector3 pathEndPosition { get; }
- //*undocumented*
+ ///Warps agent to the provided position.
+ ///Returns true if successful, otherwise returns false.
+ /// New position to warp the agent to.
+ ///True if agent is successfully warped, otherwise false.
public extern bool Warp(Vector3 newPosition);
- // Apply relative movement to current position.
+ ///Apply relative movement to current position.
+ ///If the agent has a path it will be adjusted.
+ /// The relative movement vector.
public extern void Move(Vector3 offset);
+ ///Stop movement of this agent along its current path.
+ ///See for how to resume movement after stopping.
[Obsolete("Set isStopped to true instead.")]
public extern void Stop();
- // Stop movement of this agent along its current path.
[Obsolete("Set isStopped to true instead.")]
public void Stop(bool stopUpdates) { Stop(); }
- // Resumes the movement along the current path.
+ ///Resumes the movement along the current path after a pause.
+ ///See for how to pause movement along the current path.
[Obsolete("Set isStopped to false instead.")]
public extern void Resume();
+ ///Use this property to set, or get, whether the NavMesh agent stops or continues its movement along the current path.
+ ///If set to true, the NavMesh agent's movement stops along its current path. If set to false after the NavMesh agent has stopped, the NavMesh agent resumes its movement along the current path.
public extern bool isStopped
{
[FreeFunction("NavMeshAgentScriptBindings::GetIsStopped", HasExplicitThis = true)]
@@ -150,13 +429,22 @@ public extern bool isStopped
set;
}
- // Clears the current path. Note that this agent will not start looking for a new path until SetDestination is called.
+ ///Clears the current path.
+ ///When the path is cleared, the agent will not start looking for a new path until SetDestination is called.
+ ///
+ ///Note that if the agent is on an OffMeshLink when this function is called, it will complete the link immediately.
public extern void ResetPath();
- // Assign path to this agent.
+ ///Assign a new path to this agent.
+ ///If you successfully assign the path, the agent resumes movement toward the new target.
+ ///If the path cannot be assigned, the path is cleared (see ).
+ ///A path that was calculated for a different agent type than this agent's is ignored: the method returns false and the agent keeps its current path. Use or with a NavMeshQueryFilter to obtain a path for this agent type.
+ /// New path to follow.
+ ///True if the path is successfully assigned.
public extern bool SetPath([NotNull] NavMeshPath path);
- // Set or get a copy of the current path.
+ ///Property to get and set the current path.
+ ///This property can be useful for GUI, debugging and other purposes to get the points of the path calculated by the navigation system. Additionally, a path created from user code can be set for the agent to follow in the usual way. An example of this might be a patrol route designed for coverage rather than optimal distance between two points.
public NavMeshPath path
{
get
@@ -176,14 +464,126 @@ public NavMeshPath path
[NativeMethod("CopyPath")]
internal extern void CopyPathTo([NotNull] NavMeshPath path);
- // Locate the closest NavMesh edge.
+ ///Locate the closest NavMesh edge.
+ ///The returned object contains the position
+ ///and details of the nearest point on the nearest edge of the
+ ///Navmesh. Since an edge typically corresponds to a wall or
+ ///other large object, this could be used to make a character
+ ///take cover as close to the wall as possible.
+ /// Holds the properties of the resulting location.
+ ///True if a nearest edge is found.
+ ///
+ /// ();
+ /// }
+ ///
+ /// void Update() {
+ /// if (Input.GetMouseButtonDown(0))
+ /// TakeCover();
+ /// }
+ ///
+ /// void TakeCover() {
+ /// NavMeshHit hit;
+ /// if (agent.FindClosestEdge(out hit))
+ /// agent.SetDestination(hit.position);
+ /// }
+ ///}
+ ///]]>
+ ///
[NativeName("DistanceToEdge")]
public extern bool FindClosestEdge(out NavMeshHit hit);
- // Trace movement towards a target position in the NavMesh. Without moving the agent.
+ ///Trace a straight path towards a target postion in the NavMesh without moving the agent.
+ ///This function follows the path of a "ray" between the agent's
+ ///position and the specified target position. If an obstruction is
+ ///encountered along the line then a true value is returned and
+ ///the position and other details of the obstructing object are stored
+ ///in the hit parameter. This can be used to check if there is a clear
+ ///shot or line of sight between a character and a target object.
+ ///This function is preferable to the similar
+ ///because the line tracing is performed in a simpler way using the navmesh
+ /// and has a lower processing overhead.
+ /// The desired end position of movement.
+ /// Properties of the obstacle detected by the ray (if any).
+ ///True if there is an obstacle between the agent and the target position, otherwise false.
+ ///
+ /// ();
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// NavMeshHit hit;
+ /// if (!agent.Raycast(target.position, out hit))
+ /// {
+ /// // Target is "visible" from our position.
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
public extern bool Raycast(Vector3 targetPosition, out NavMeshHit hit);
- // Calculate a path to a specified point and store the resulting path.
+ ///Calculate a path to a specified point and store the resulting path.
+ ///Use this function to avoid gameplay delays by planning a path before it is needed. You can also use this function to check if a target position is reachable before moving the agent. The function takes into account the agent's , and area costs properties when it searches for a matching path.
+ ///
+ ///This function is synchronous. It performs path finding immediately, which can adversely affect the frame rate when processing very long paths. It is recommended to only perform a few path finds per frame when, for example, evaluating distances to cover points.
+ ///
+ ///Use the returned path to set the path for this agent, or an agent of the same type, with . For SetPath to work, the agent must be close to the starting point and be allowed to move through the area type where the start point is.
+ /// The final position of the path requested.
+ /// The resulting path.
+ ///True if either a complete or partial path is found. False otherwise.
+ ///
+ /// ();
+ /// var path = new NavMeshPath();
+ /// agent.CalculatePath(target.position, path);
+ /// switch (path.status)
+ /// {
+ /// case NavMeshPathStatus.PathComplete:
+ /// Debug.Log($"{agent.name} will be able to reach {target.name}.");
+ /// break;
+ /// case NavMeshPathStatus.PathPartial:
+ /// Debug.LogWarning($"{agent.name} will only be able to move partway to {target.name}.");
+ /// break;
+ /// default:
+ /// Debug.LogError($"There is no valid path for {agent.name} to reach {target.name}.");
+ /// break;
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
public bool CalculatePath(Vector3 targetPosition, NavMeshPath path)
{
path.ClearCorners();
@@ -193,63 +593,152 @@ public bool CalculatePath(Vector3 targetPosition, NavMeshPath path)
[FreeFunction("NavMeshAgentScriptBindings::CalculatePathInternal", HasExplicitThis = true)]
extern bool CalculatePathInternal(Vector3 targetPosition, [NotNull] NavMeshPath path);
- // Sample a position along the current path.
+ ///Sample a position along the current path.
+ ///This function looks ahead the specified maxDistance along the current path, up to the third
+ /// corner . It returns details of the mesh
+ /// at that position in a object. You can use this
+ /// to check the type of surface that lies ahead before the character gets there. For example, characters could
+ /// raise their guns above their heads if they are about to wade through water.
+ ///
+ /// If the path sampling terminates on an outer edge, hit.mask is 0. If the path sampling terminates at an area not specified by areaMask , hit.mask contains the area mask of the blocking polygon. If the sampling reaches the end of the path, or the limit at the path's third corner, hit.mask contains the area mask at that position on the NavMesh.
+ /// A bitfield mask specifying which NavMesh areas can be passed when tracing the path.
+ /// Terminate scanning the path at this distance.
+ /// Holds the properties of the resulting location.
+ ///True if terminated before reaching the position at maxDistance , false otherwise.
+ ///
+ /// ();
+ /// waterMask = 1 << NavMesh.GetAreaFromName("Water");
+ /// agent.SetDestination(target.position);
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// NavMeshHit hit;
+ ///
+ /// // Check all areas one length unit ahead.
+ /// if (!agent.SamplePathPosition(NavMesh.AllAreas, 1.0F, out hit))
+ /// if ((hit.mask & waterMask) != 0)
+ /// {
+ /// // Water detected along the path...
+ /// }
+ /// }
+ ///}
+ ///]]>
+ ///
public extern bool SamplePathPosition(int areaMask, float maxDistance, out NavMeshHit hit);
+ ///Sets the cost for traversing over geometry of the layer type.
+ ///If you enable or disable the agent then the cost will be reset to the default layer cost.
+ /// Layer index.
+ /// New cost for the specified layer.
[Obsolete("Use SetAreaCost instead.")]
[NativeMethod("SetAreaCost")]
public extern void SetLayerCost(int layer, float cost);
+ ///Gets the cost for crossing ground of a particular type.
+ ///The cost of a path is the amount of "difficulty" involved in following it - the shortest path may not be the quickest if it passes over difficult ground, such as mud, snow, etc. Different types of ground are denoted by navmesh layers in Unity. The cost of a particular layer is given in cost units per distance unit. Note that the cost of a path applies to the pathfinding only and does not automatically affect the movement speed of the agent when following the path. Indeed, the path's cost may denote other factors such as danger (safe but long path through a minefield) or visibility (long path that keeps a character in the shadows).
+ /// Layer index.
+ ///Current cost of specified layer.
[Obsolete("Use GetAreaCost instead.")]
[NativeMethod("GetAreaCost")]
public extern float GetLayerCost(int layer);
+ ///Sets the cost for traversing over areas of the area type.
+ ///If you enable or disable the agent then the cost will be reset to the default layer cost.
+ /// Area cost.
+ /// New cost for the specified area index.
public extern void SetAreaCost(int areaIndex, float areaCost);
+ ///Gets the cost for path calculation when crossing area of a particular type.
+ ///The cost of a path is the amount of "difficulty" involved in calculating it - the shortest path may not be the best if it passes over difficult ground, such as mud, snow, etc. Different types of areas are denoted by navmesh areas in Unity. The cost of a particular area is given in cost units per distance unit. Note that the cost of a path applies to the pathfinding only and does not automatically affect the movement speed of the agent when following the path. Indeed, the path's cost may denote other factors such as danger (safe but long path through a minefield) or visibility (long path that keeps a character in the shadows).
+ /// Area Index.
+ ///Current cost for specified area index.
public extern float GetAreaCost(int areaIndex);
+ ///Returns the owning object of the NavMesh the agent is currently placed on.
+ ///If no owner is set for a NavMesh or link instance the return value is null.
+ ///
+ ///
public Object navMeshOwner => GetOwnerInternal();
+ ///The type ID for the agent.
+ ///This identifier determines which NavMeshes are available for the Agent to move on. See also . Changing this ID will reset the Agent's current path.
public extern int agentTypeID { get; set; }
[NativeName("GetCurrentPolygonOwner")]
extern Object GetOwnerInternal();
+ ///Specifies which NavMesh layers are passable (bitfield). Changing walkableMask will make the path stale (see ).
[Obsolete("Use areaMask instead.")]
public int walkableMask { get { return areaMask; } set { areaMask = value; } }
+ ///Specifies which NavMesh areas are passable. Changing areaMask will make the path stale (see ).
+ ///This is a bitfield.
public extern int areaMask { get; set; }
- // Maximum movement speed.
+ ///Maximum movement speed when following a path.
+ ///An agent will typically need to speed up and slow down as it follows a path (eg, it will slow down to make a tight turn). The speed is often limited by the length of a path segment and the time taken to accelerate and brake, but the speed will not exceed the value set by this property even on a long, straight path.
public extern float speed { get; set; }
- // Maximum rotation speed in (deg/s).
+ ///Maximum turning speed in (deg/s) while following a path.
+ ///This is the maximum rate at which the agent can turn as it rounds the "corner" defined by a waypoint. The actual turning circle is also influenced by the speed of the agent on approach and also the maximum acceleration.
public extern float angularSpeed { get; set; }
- // Maximum acceleration.
+ ///The maximum acceleration of an agent as it follows a path, given in units / sec^2.
+ ///An agent does not follow precisely the line segments of the path calculated by the navigation system but rather uses the waypoints along the path as intermediate destinations. This value is the maximum amount by which the agent can accelerate while moving towards the next waypoint.
public extern float acceleration { get; set; }
- // Should the agent update the transform position.
+ ///Gets or sets whether the transform position is synchronized with the simulated agent position. The default value is true.
+ ///When true: changing the transform position will affect the simulated position and vice-versa.
+ ///
+ ///When false: the simulated position will not be applied to the transform position and vice-versa.
+ ///
+ ///Setting to false can be used to enable explicit control of the transform position via script.
+ ///This allows you to use the agent's simulated position to drive another component, which in turn sets the transform position (eg. animation with root motion or physics).
+ ///
+ ///When enabling the (from previously being disabled), the transform will be moved to the simulated position. This way the agent stays constrained to the navmesh surface.
public extern bool updatePosition { get; set; }
- // Should the agent update the transform orientation.
+ ///Should the agent update the transform orientation?
public extern bool updateRotation { get; set; }
+ ///Allows you to specify whether the agent should be aligned to the up-axis of the NavMesh or link that it is placed on.
+ ///When this value is set to true, the agent will always be aligned to the local up-axis of the NavMesh or link that it is currently on. When set to false, the agent’s orientation is unaffected by the orientation of the NavMesh.
public extern bool updateUpAxis { get; set; }
- // Agent avoidance radius.
+ ///The avoidance radius for the agent.
+ ///This is the agent's "personal space" within which obstacles and other agents should not pass.
public extern float radius { get; set; }
- // Agent height.
+ ///The height of the agent for purposes of passing under obstacles, etc.
public extern float height { get; set; }
- // The level of quality of avoidance.
+ ///The level of quality of avoidance.
+ ///This property lets you trade off the precision of obstacle avoidance againt the processor load required to achieve it. The exact quality/performance values will depend heavily on the complexity of the Scene but as a general rule, faster performance can be achieved at the cost of quality and vice versa.
public extern ObstacleAvoidanceType obstacleAvoidanceType { get; set; }
- // The avoidance priority level.
+ ///The avoidance priority level.
+ ///When the agent is performing avoidance, agents of lower priority are ignored.
+ ///The valid range is from 0 to 99 where:
+ ///Most important = 0. Least important = 99. Default = 50.
public extern int avoidancePriority { get; set; }
- // Is agent mapped to navmesh
+ ///Is the agent currently bound to the navmesh?
+ ///This property is false if the agent, for some reason, could not bind to the navmesh. E.g. if Scene has no navmesh.
public extern bool isOnNavMesh
{
[NativeName("InCrowdSystem")]
diff --git a/Modules/AI/Components/NavMeshObstacle.bindings.cs b/Modules/AI/Components/NavMeshObstacle.bindings.cs
index 954da638c3..eae5967032 100644
--- a/Modules/AI/Components/NavMeshObstacle.bindings.cs
+++ b/Modules/AI/Components/NavMeshObstacle.bindings.cs
@@ -8,49 +8,148 @@
namespace UnityEngine.AI
{
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Shape of the obstacle.
[MovedFrom("UnityEngine")]
public enum NavMeshObstacleShape
{
- // Capsule shaped obstacle.
+ ///Capsule shaped obstacle.
Capsule = 0,
- // Box shaped obstacle.
+ ///Box shaped obstacle.
Box = 1,
}
- // Navigation mesh obstacle.
+ ///An obstacle for NavMeshAgents to avoid.
+ ///A NavMeshObstacle is cylindrical in shape and can move around the surface of the NavMesh with a specified velocity. By default, the obstacle will only affect the agent's avoidance behaviour rather than the pathfinding. This means that the agent will ignore the obstacle when plotting a path but will sidestep around it while moving along the path. If carving is enabled, the obstacle will create a temporary "hole" in the NavMesh. The hole will be recognised by the pathfinding, so paths will be plotted to avoid the obstacle. This means that if, say, an obstacle blocks a narrow gap, the pathfinding will seek an alternative route to the target. Without carving, the agent will head for the gap but won't be able to pass until the obstacle is clear.
+ ///
[MovedFrom("UnityEngine")]
[NativeHeader("Modules/AI/Components/NavMeshObstacle.bindings.h")]
[HelpURL("https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/NavMeshObstacle.html")]
public sealed class NavMeshObstacle : Behaviour
{
- // Obstacle height.
+ ///Height of the obstacle's cylinder shape.
+ ///
public extern float height { get; set; }
- // Obstacle radius.
+ ///Radius of the obstacle's capsule shape.
+ ///
public extern float radius { get; set; }
- // Obstacle velocity.
+ ///Velocity at which the obstacle moves around the NavMesh.
+ ///
+ ///
+ /// /// Update the current GameObject's NavMesh Obstacle velocity according to its position changes.
+ /// /// Useful when the position of an object is controlled by script.
+ /// ///
+ /// public class ManualObstacleVelocityUpdater : MonoBehaviour
+ /// {
+ /// NavMeshObstacle m_Obstacle;
+ /// Vector3 m_LastPosition;
+ ///
+ /// void Start()
+ /// {
+ /// m_Obstacle = GetComponent();
+ /// m_LastPosition = transform.position;
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// var deltaTime = Time.deltaTime;
+ /// if (m_Obstacle && deltaTime > Mathf.Epsilon)
+ /// {
+ /// // Compute this frame's velocity
+ /// var newPosition = transform.position;
+ /// var velocity = (newPosition - m_LastPosition) / deltaTime;
+ /// m_Obstacle.velocity = velocity;
+ ///
+ /// // Keep track of the last considered position
+ /// m_LastPosition = newPosition;
+ /// }
+ /// }
+ /// }
+ ///]]>
+ ///
public extern Vector3 velocity { get; set; }
- // Enable carving
+ ///Should this obstacle make a cut-out in the navmesh.
+ ///When enabled, this changes the navmesh by cutting out a hole. The shape of the hole is based on the size and shape set on and the navmesh bake settings for radius and height.
+ ///
+ ///When the obstacle moves, the carved hole will also move but to reduce CPU overhead the hole is only recalculated when necessary. The recalculation logic has two options: 1) carve when stationary, 2) carve when moved.
+ ///
+ ///"Carve when stationary" is the default behavior and is used when is set to true. The obstacle is treated as moving when it has moved more than the distance set by . At this time, the carved hole is removed. When the obstacle has stopped moving, and has been stationary more than seconds, the obstacles is treated stationary and carving is updated again. While the obstacle is moving, the agents will avoid it using the collision avoidance, but will not plan paths around it. This mode is generally the best choice in terms of performance. It is good match when the game object is controlled by physics (i.e. crates and barrels).
+ ///
+ ///"Carve when moved" behavior is used when is set to false. In this mode the carved hole is updated when the obstacle has moved more than the distance set by . This mode is well suited for large slowly moving obstacles, for example a tank that is being avoided by infantry.
public extern bool carving { get; set; }
- // When carving enabled, carve only when obstacle is stationary, moving obstacles are avoided dynamically.
+ ///Should this obstacle be carved when it is constantly moving?
+ ///When this property is enabled, the obstacle will carve a hole only when it is stationary. There will be no hole carved when the object is moving. See for full description of different carving behaviors.
public extern bool carveOnlyStationary { get; set; }
- // Update carving if moved at least this distance, or if carveWhenStationary if moved at least this distance, the obstacle is considered moving.
+ ///Threshold distance for updating a moving carved hole (when carving is enabled).
+ ///If the has moved a distance shorter than the threshold since last carving then the navmesh will not be updated.
+ ///
[NativeProperty("MoveThreshold")]
public extern float carvingMoveThreshold { get; set; }
- // If carveWhenStationary is set, the obstacle is considered stationary if it has not moved during this long period.
+ ///Time to wait until obstacle is treated as stationary (when carving and carveOnlyStationary are enabled).
+ ///If the has been moving, and becomes still, We wait carvingTimeToStationary time until the obstacle is treated stationary by the carving system. See for full description of different carving behaviors.
[NativeProperty("TimeToStationary")]
public extern float carvingTimeToStationary { get; set; }
- // Shape of the obstacle, NavMeshObstacleShape.Box or NavMeshObstacleShape.Capsule.
+ ///The shape of the obstacle.
+ ///
+ /// Set or get the shape of the .
+ /// A newly created has a shape of the shape.
+ /// The obstacle shapes are listed in .
+ /// **Note:** When the shape is changed the is set back to zero.
+ ///
public extern NavMeshObstacleShape shape { get; set; }
+ ///The center of the obstacle, measured in the object's local space.
+ ///**Note:** When a is created the is set to zero.
+ ///
+ /// ();
+ /// Debug.Log(navMeshObstacle.center);
+ /// }
+ ///}
+ ///]]>
+ ///
public extern Vector3 center { get; set; }
+ ///The size of the obstacle, measured in the object's local space.
+ ///The size will be scaled by the transform's scale.
+ ///
+ /// ();
+ /// Mesh mesh = GetComponent().mesh;
+ /// obstacle.shape = NavMeshObstacleShape.Box;
+ /// obstacle.size = mesh.bounds.size;
+ /// }
+ ///}
+ ///]]>
+ ///
public extern Vector3 size
{
[FreeFunction("NavMeshObstacleScriptBindings::GetSize", HasExplicitThis = true)]
diff --git a/Modules/AI/Components/OffMeshLink.bindings.cs b/Modules/AI/Components/OffMeshLink.bindings.cs
index 6697143c69..a78dfec5d1 100644
--- a/Modules/AI/Components/OffMeshLink.bindings.cs
+++ b/Modules/AI/Components/OffMeshLink.bindings.cs
@@ -9,22 +9,24 @@
namespace UnityEngine.AI
{
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
- // Link type specifier.
+ ///Link type specifier.
[MovedFrom("UnityEngine")]
public enum OffMeshLinkType
{
- // Manually specified type of link.
+ ///Manually specified type of link.
LinkTypeManual = 0,
- // Vertical drop.
+ ///Vertical drop.
LinkTypeDropDown = 1,
- // Horizontal jump.
+ ///Horizontal jump.
LinkTypeJumpAcross = 2
}
// Keep this struct in sync with the one defined in "NavMeshBindingTypes.h"
- // State of OffMeshLink.
+ ///State of OffMeshLink.
+ ///
+ ///
[MovedFrom("UnityEngine")]
[NativeHeader("Modules/AI/Components/OffMeshLink.bindings.h")]
public partial struct OffMeshLinkData
@@ -36,22 +38,30 @@ public partial struct OffMeshLinkData
internal Vector3 m_StartPos;
internal Vector3 m_EndPos;
- // Is link valid (RO).
+ ///Is link valid (RO).
public bool valid => m_Valid != 0;
- // Is link active (RO).
+ ///Is link active (RO).
public bool activated => m_Activated != 0;
- // Link type specifier (RO).
+ ///Link type specifier (RO).
public OffMeshLinkType linkType => m_LinkType;
- // Link start world position (RO).
+ ///Link start world position (RO).
public Vector3 startPos => m_StartPos;
- // Link end world position (RO).
+ ///Link end world position (RO).
public Vector3 endPos => m_EndPos;
- // The object that created this link instance if the link type is a manually placed [[Offmeshlink]] or [[NavMeshLinkData]] (RO).
+ ///Get the object used to create the NavMesh link represented by the data in this struct.
+ ///If the link has been instantiated by a call to then this property returns the object that might have been associated to that instance with a call to . If that link instance has no owner assigned to it then this property returns null.
+ ///
+ ///If the link was instantiated by an component then the owner returns a reference to that component.
+ ///
+ ///To effectively use this property in your scripts you need to determine the exact type of the returned object. To do that cast the object to the types that you use in your project to create NavMesh links.
+ ///
+ ///For automatically-generated Jump or Drop links, this property returns null.
+ ///
public Object owner => GetLinkOwnerInternal(m_InstanceID);
[FreeFunction("OffMeshLinkScriptBindings::GetLinkOwnerInternal")]
diff --git a/Modules/AI/Components/OffMeshLink.deprecated.cs b/Modules/AI/Components/OffMeshLink.deprecated.cs
index 869fbb4517..09a32d9b13 100644
--- a/Modules/AI/Components/OffMeshLink.deprecated.cs
+++ b/Modules/AI/Components/OffMeshLink.deprecated.cs
@@ -10,7 +10,8 @@ namespace UnityEngine.AI
{
public partial struct OffMeshLinkData
{
- // The [[OffMeshLink]] if the link type is a manually placed Offmeshlink (RO).
+ ///The if the link type is a manually placed Offmeshlink (RO).
+ ///Automatically generated Jump and drop links will return null.
[Obsolete("offMeshLink has been deprecated. Use 'owner' instead.")]
public OffMeshLink offMeshLink => GetOffMeshLinkInternal(m_InstanceID);
@@ -20,42 +21,55 @@ public partial struct OffMeshLinkData
#pragma warning restore CS0618
}
- // Link allowing movement outside the planar navigation mesh.
+ ///Link allowing movement outside the planar navigation mesh.
[MovedFrom("UnityEngine")]
[Obsolete("The OffMeshLink component is no longer supported and will be removed. Use NavMeshLink instead.")]
[HelpURL("https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/OffMeshLink.html")]
public sealed class OffMeshLink : Behaviour
{
- // Is link active.
+ ///Is link active.
[Obsolete("activated has been deprecated together with the class. Declare the object as NavMeshLink and use activated as before.")]
public extern bool activated { get; set; }
- // Is link occupied. (RO)
+ ///Is link occupied. (RO)
[Obsolete("occupied has been deprecated together with the class. Declare the object as NavMeshLink and use occupied as before.")]
public extern bool occupied { get; }
- // Modify pathfinding cost for the link.
+ ///Modify pathfinding cost for the link.
+ ///When the costOverride value is non-negative the cost of moving over the OffMeshLink
+ ///is equivalent to the costOverride value times the Euclidean distance
+ ///between OffMeshLink end points.
[Obsolete("costOverride has been deprecated together with the class. Declare the object as NavMeshLink and use costModifier instead.")]
public extern float costOverride { get; set; }
+ ///Can link be traversed in both directions.
+ ///When false the link can only be traversed from start to end.
[Obsolete("biDirectional has been deprecated together with the class. Declare the object as NavMeshLink and use bidirectional instead.")]
public extern bool biDirectional { get; set; }
+ ///Explicitly update the link endpoints.
+ ///Updates the OffMeshLink endpoints to match the transforms specified by the start and end transforms.
[Obsolete("UpdatePositions() has been deprecated together with the class. Declare the object as NavMeshLink and use UpdateLink() instead.")]
public extern void UpdatePositions();
+ ///NavMeshLayer for this OffMeshLink component.
[Obsolete("navMeshLayer has been deprecated together with the class. Declare the object as NavMeshLink and use area instead. (UnityUpgradable) -> area")]
public int navMeshLayer { get { return area; } set { area = value; } }
+ ///NavMesh area index for this OffMeshLink component.
[Obsolete("area has been deprecated together with the class. Declare the object as NavMeshLink and use area as before.")]
public extern int area { get; set; }
+ ///Automatically update endpoints.
+ ///The OffMeshLink component will try to match endpoint transforms specified by and . See also .
[Obsolete("autoUpdatePositions has been deprecated together with the class. Declare the object as NavMeshLink and use autoUpdate instead.")]
public extern bool autoUpdatePositions { get; set; }
+ ///The transform representing link start position.
[Obsolete("startTransform has been deprecated together with the class. Declare the object as NavMeshLink and use startTransform as before.")]
public extern Transform startTransform { get; set; }
+ ///The transform representing link end position.
[Obsolete("endTransform has been deprecated together with the class. Declare the object as NavMeshLink and use endTransform as before.")]
public extern Transform endTransform { get; set; }
}
diff --git a/Modules/AI/LowLevel/Scripting/Bindings/NavLocation.bindings.cs b/Modules/AI/LowLevel/Scripting/Bindings/NavLocation.bindings.cs
index df3ef46251..ba756a2843 100644
--- a/Modules/AI/LowLevel/Scripting/Bindings/NavLocation.bindings.cs
+++ b/Modules/AI/LowLevel/Scripting/Bindings/NavLocation.bindings.cs
@@ -8,9 +8,13 @@
namespace Unity.AI.Navigation.LowLevel;
+///A position mapped to a navigation node.
+
public readonly struct NavLocation : IEquatable
{
+ ///
public NavNode node { get; }
+ ///
public Vector3 position { get; }
internal NavLocation(Vector3 position, NavNode node)
@@ -19,30 +23,35 @@ internal NavLocation(Vector3 position, NavNode node)
this.node = node;
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator ==(NavLocation left, NavLocation right)
{
return left.node.Equals(right.node) && left.position.Equals(right.position);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator !=(NavLocation left, NavLocation right)
{
return !left.node.Equals(right.node) || !left.position.Equals(right.position);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly bool Equals(NavLocation other)
{
return node.Equals(other.node) && position.Equals(other.position);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override bool Equals(object obj)
{
return obj is NavLocation other && node.Equals(other.node) && position.Equals(other.position);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override int GetHashCode()
{
diff --git a/Modules/AI/LowLevel/Scripting/Bindings/NavLowLevelTypes.bindings.cs b/Modules/AI/LowLevel/Scripting/Bindings/NavLowLevelTypes.bindings.cs
index e186a6cf00..11f1de0418 100644
--- a/Modules/AI/LowLevel/Scripting/Bindings/NavLowLevelTypes.bindings.cs
+++ b/Modules/AI/LowLevel/Scripting/Bindings/NavLowLevelTypes.bindings.cs
@@ -7,26 +7,41 @@
namespace Unity.AI.Navigation.LowLevel;
// Keep in sync with the values in NavMeshTypes.h
+///The state of a navigation query after running an operation.
+
[Flags]
public enum NavQueryStatus
{
// High level status.
+ ///
Failure = 1 << 31,
+ ///
Success = 1 << 30,
+ ///
InProgress = 1 << 29,
// Detail information for status.
+ ///
StatusDetailMask = 0x0ffffff,
+ ///
InvalidParameter = 1 << 3, // An input parameter was invalid.
+ ///
MoreDataAvailable = 1 << 4, // Result buffer for the query was too small to store all results.
+ ///
MaxNodesToVisitExceeded = 1 << 5, // Query ran out of nodes during search.
+ ///
PartialResult = 1 << 6 // Query did not reach the end location, returning best guess.
}
// Flags describing node properties. Keep in sync with the enum declared in NavMesh.h
+///Describes whether the navigation node is created by a NavMesh or a NavMeshLink.
+
public enum NavNodeType
{
+ ///
Undefined = -1,
+ ///
Polygon = 0, // Regular ground polygons.
+ ///
Link = 1 // Off-mesh connections.
}
diff --git a/Modules/AI/LowLevel/Scripting/Bindings/NavNode.bindings.cs b/Modules/AI/LowLevel/Scripting/Bindings/NavNode.bindings.cs
index dcd7133ebd..1aff230eca 100644
--- a/Modules/AI/LowLevel/Scripting/Bindings/NavNode.bindings.cs
+++ b/Modules/AI/LowLevel/Scripting/Bindings/NavNode.bindings.cs
@@ -8,28 +8,36 @@
namespace Unity.AI.Navigation.LowLevel;
+///Node in the navigation graph.
+
public struct NavNode : IEquatable
{
internal ulong m_PolyRef;
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator ==(NavNode left, NavNode right) { return left.m_PolyRef == right.m_PolyRef; }
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator !=(NavNode left, NavNode right) { return left.m_PolyRef != right.m_PolyRef; }
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly bool Equals(NavNode other) { return m_PolyRef == other.m_PolyRef; }
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override bool Equals(object obj)
{
return obj is NavNode other && m_PolyRef == other.m_PolyRef;
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override int GetHashCode() { return m_PolyRef.GetHashCode(); }
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly bool IsNull() { return m_PolyRef == 0; }
}
diff --git a/Modules/AI/LowLevel/Scripting/Bindings/NavQueryBuffer.bindings.cs b/Modules/AI/LowLevel/Scripting/Bindings/NavQueryBuffer.bindings.cs
index 41d9f2eea5..42f8a962f5 100644
--- a/Modules/AI/LowLevel/Scripting/Bindings/NavQueryBuffer.bindings.cs
+++ b/Modules/AI/LowLevel/Scripting/Bindings/NavQueryBuffer.bindings.cs
@@ -12,6 +12,8 @@
namespace Unity.AI.Navigation.LowLevel;
+///Object used for doing navigation operations in a .
+
[NativeContainer]
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/AI/LowLevel/NavWorld.bindings.h")]
@@ -28,7 +30,7 @@ public struct NavQueryBuffer : IDisposable, IEquatable
internal readonly bool isNull => m_NavMeshQuery == IntPtr.Zero;
internal AtomicSafetyHandle m_Safety;
- internal uint m_SafetyOpenListId;
+ internal uint m_SafetyUniqueId;
internal static readonly int k_StaticSafetyId = AtomicSafetyHandle.NewStaticSafetyId();
@@ -44,6 +46,7 @@ public struct NavQueryBuffer : IDisposable, IEquatable
// Keep in sync with kMaxNavMeshNodePoolSize = USHRT_MAX from NavMeshNode.h
const int k_MaxNavMeshNodePoolSize = ushort.MaxValue;
+ ///
public NavQueryBuffer(NavWorld world, Allocator allocator, int maxNodesToVisit = 1024)
{
if (!world.IsValid())
@@ -80,12 +83,13 @@ public NavQueryBuffer(NavWorld world, Allocator allocator, int maxNodesToVisit =
AddQuerySafety(m_NavMeshQuery, m_Safety);
- m_SafetyOpenListId = GetOpenListId(m_NavMeshQuery);
- var brokenNodePoolInit = m_SafetyOpenListId == 0;
+ m_SafetyUniqueId = GetUniqueId(m_NavMeshQuery);
+ var brokenNodePoolInit = m_SafetyUniqueId == 0;
if (brokenNodePoolInit)
- m_SafetyOpenListId = uint.MaxValue;
+ m_SafetyUniqueId = uint.MaxValue;
}
+ ///
[WriteAccessRequired]
public void Dispose()
{
@@ -120,39 +124,44 @@ public void Dispose()
static extern void Destroy(IntPtr navMeshQuery);
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator ==(NavQueryBuffer left, NavQueryBuffer right)
{
return left.Equals(right);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator !=(NavQueryBuffer left, NavQueryBuffer right)
{
return !left.Equals(right);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly bool Equals(NavQueryBuffer other)
{
var pointersEqual = m_NavMeshQuery == other.m_NavMeshQuery && m_NavMeshUniqueId == other.m_NavMeshUniqueId;
- pointersEqual = pointersEqual && m_SafetyOpenListId == other.m_SafetyOpenListId;
+ pointersEqual = pointersEqual && m_SafetyUniqueId == other.m_SafetyUniqueId;
return pointersEqual;
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override bool Equals(object obj)
{
return obj is NavQueryBuffer other && Equals(other);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override int GetHashCode()
{
var hashCode = HashCode.Combine(m_NavMeshQuery, m_NavMeshUniqueId);
- hashCode = HashCode.Combine(hashCode, m_SafetyOpenListId);
+ hashCode = HashCode.Combine(hashCode, m_SafetyUniqueId);
return hashCode;
}
@@ -160,7 +169,7 @@ public readonly override int GetHashCode()
static extern void RemoveQuerySafety(IntPtr navMeshQuery, AtomicSafetyHandle handle);
[NativeMethod(IsThreadSafe = true)]
- static extern uint GetOpenListId(IntPtr navMeshQuery);
+ static extern uint GetUniqueId(IntPtr navMeshQuery);
[NativeMethod(IsThreadSafe = true)]
static extern bool HasNodePool(IntPtr navMeshQuery);
@@ -189,8 +198,8 @@ internal readonly void CheckValidAndThrow()
throw new ObjectDisposedException(k_NoInternalQueryAllocatedErrorMessage);
}
- var safetyIdAtKnownAddress = GetOpenListId(m_NavMeshQuery);
- if (safetyIdAtKnownAddress != m_SafetyOpenListId)
+ var currentUniqueId = GetUniqueId(m_NavMeshQuery);
+ if (currentUniqueId != m_SafetyUniqueId)
throw new ObjectDisposedException(k_NoInternalQueryAllocatedErrorMessage);
}
}
diff --git a/Modules/AI/LowLevel/Scripting/Bindings/NavWorld.bindings.cs b/Modules/AI/LowLevel/Scripting/Bindings/NavWorld.bindings.cs
index d8ba060157..402d2c4e5d 100644
--- a/Modules/AI/LowLevel/Scripting/Bindings/NavWorld.bindings.cs
+++ b/Modules/AI/LowLevel/Scripting/Bindings/NavWorld.bindings.cs
@@ -24,6 +24,8 @@ struct NavMeshPointers
public uint m_UniqueId;
}
+///Assembles together a collection of NavMesh surfaces and links that are used as a whole for performing navigation operations.
+
[NativeContainer]
[NativeContainerIsReadOnly]
[StructLayout(LayoutKind.Sequential)]
@@ -52,6 +54,7 @@ public struct NavWorld : IDisposable, IEquatable
[NativeMethod(IsThreadSafe = true)]
static extern bool IsValidWorldInternal(IntPtr navMesh, IntPtr immutableQuery, uint uniqueId);
+ ///
public readonly bool IsValid()
{
return m_NavMeshPtr != IntPtr.Zero
@@ -61,6 +64,7 @@ public readonly bool IsValid()
static extern NavMeshPointers GetDefaultWorldInternal();
+ ///
public static NavWorld GetDefaultWorld()
{
var pointers = GetDefaultWorldInternal();
@@ -85,6 +89,7 @@ public static NavWorld GetDefaultWorld()
// Explicit cleanup of the safety handle which is otherwise
// removed only when the underlying NavMesh is destroyed.
+ ///
public void Dispose()
{
if (AtomicSafetyHandle.IsValidNonDefaultHandle(m_Safety))
@@ -100,18 +105,21 @@ public void Dispose()
m_ImmutableQuery = IntPtr.Zero;
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator ==(NavWorld left, NavWorld right)
{
return left.Equals(right);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public static bool operator !=(NavWorld left, NavWorld right)
{
return !left.Equals(right);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly bool Equals(NavWorld other)
{
@@ -120,12 +128,14 @@ public readonly bool Equals(NavWorld other)
&& m_UniqueId == other.m_UniqueId;
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override bool Equals(object obj)
{
return obj is NavWorld other && Equals(other);
}
+ ///
[MethodImpl(MethodImplOptionsEx.AggressiveInlining)]
public readonly override int GetHashCode()
{
@@ -156,6 +166,7 @@ readonly void CheckBufferMatchAndThrow(NavQueryBuffer queryBuffer)
static extern void AddDependencyInternal(IntPtr navMesh, JobHandle handle);
+ ///
public readonly void AddDependency(JobHandle job)
{
CheckValidPtrAndThrow();
@@ -169,6 +180,7 @@ public readonly void AddDependency(JobHandle job)
static extern NavLocation MapLocation(IntPtr navMeshQuery, Vector3 position, Vector3 extents,
int agentTypeID, int areaMask = NavMesh.AllAreas);
+ ///
public readonly NavLocation MapLocation(Vector3 position, Vector3 extents, int agentTypeId,
int areaMask = NavMesh.AllAreas)
{
@@ -176,6 +188,7 @@ public readonly NavLocation MapLocation(Vector3 position, Vector3 extents, int a
return MapLocation(m_ImmutableQuery, position, extents, agentTypeId, areaMask);
}
+ ///
public readonly unsafe NavQueryStatus BeginFindPath(NavQueryBuffer queryBuffer,
NavLocation start, NavLocation end,
int areaMask = NavMesh.AllAreas, NativeArray costs = new())
@@ -229,6 +242,7 @@ public readonly unsafe NavQueryStatus BeginFindPath(NavQueryBuffer queryBuffer,
return BeginFindPath(queryBuffer.navMeshQueryPtr, start, end, areaMask, costsPtr);
}
+ ///
public readonly NavQueryStatus ContinueFindPath(NavQueryBuffer queryBuffer, int nodesToVisit, out int nodesVisited)
{
CheckValidPtrAndThrow();
@@ -246,6 +260,7 @@ public readonly NavQueryStatus ContinueFindPath(NavQueryBuffer queryBuffer, int
return ContinueFindPath(queryBuffer.navMeshQueryPtr, nodesToVisit, out nodesVisited);
}
+ ///
public readonly NavQueryStatus EndFindPath(NavQueryBuffer queryBuffer, out int pathSize)
{
CheckValidPtrAndThrow();
@@ -263,6 +278,7 @@ public readonly NavQueryStatus EndFindPath(NavQueryBuffer queryBuffer, out int p
return EndFindPath(queryBuffer.navMeshQueryPtr, out pathSize);
}
+ ///
public readonly unsafe int GetResultFromFindPath(NavQueryBuffer queryBuffer, NativeSlice path)
{
CheckValidPtrAndThrow();
@@ -293,12 +309,14 @@ static extern unsafe NavQueryStatus BeginFindPath(IntPtr navMeshQuery,
[NativeMethod(IsThreadSafe = true)]
static extern bool IsValidNode(IntPtr navMeshPtr, NavNode node);
+ ///
public readonly bool IsValid(NavNode node)
{
CheckValidPtrAndThrow();
return node.m_PolyRef != 0 && IsValidNode(m_NavMeshPtr, node);
}
+ ///
public readonly bool IsValid(NavLocation location)
{
return IsValid(location.node);
@@ -307,6 +325,7 @@ public readonly bool IsValid(NavLocation location)
[NativeMethod(IsThreadSafe = true)]
static extern int GetAgentTypeIdForNode(IntPtr navMeshPtr, NavNode node);
+ ///
public readonly int GetAgentTypeIdForNode(NavNode node)
{
CheckValidPtrAndThrow();
@@ -316,6 +335,7 @@ public readonly int GetAgentTypeIdForNode(NavNode node)
[NativeMethod(IsThreadSafe = true)]
static extern int GetAreaIndexForNode(IntPtr navMeshPtr, NavNode node);
+ ///
public readonly int GetAreaIndexForNode(NavNode node)
{
CheckValidPtrAndThrow();
@@ -326,6 +346,7 @@ public readonly int GetAreaIndexForNode(NavNode node)
static extern NavQueryStatus GetClosestPointOnPoly(IntPtr navMeshQuery, NavNode node, Vector3 position,
out Vector3 nearest);
+ ///
public readonly NavLocation CreateLocation(Vector3 position, NavNode node)
{
CheckValidPtrAndThrow();
@@ -339,6 +360,7 @@ public readonly NavLocation CreateLocation(Vector3 position, NavNode node)
static extern unsafe void MoveLocations(IntPtr navMeshQuery, void* locations, void* targets, void* areaMasks,
int count);
+ ///
public readonly unsafe void MoveLocations(NativeSlice locations, NativeSlice destinations,
NativeSlice areaMasks)
{
@@ -354,6 +376,7 @@ public readonly unsafe void MoveLocations(NativeSlice locations, Na
static extern unsafe void MoveLocationsInSameAreas(IntPtr navMeshQuery, void* locations, void* targets,
int count, int areaMask);
+ ///
public readonly unsafe void MoveLocations(NativeSlice locations,
NativeSlice destinations, int areaMask = NavMesh.AllAreas)
{
@@ -369,6 +392,7 @@ public readonly unsafe void MoveLocations(NativeSlice locations,
static extern NavLocation MoveLocation(IntPtr navMeshQuery, NavLocation location, Vector3 target,
int areaMask);
+ ///
public readonly NavLocation MoveLocation(NavLocation location, Vector3 destination, int areaMask = NavMesh.AllAreas)
{
CheckValidPtrAndThrow();
@@ -379,6 +403,7 @@ public readonly NavLocation MoveLocation(NavLocation location, Vector3 destinati
static extern bool GetPortalPoints(IntPtr navMeshQuery, NavNode node, NavNode neighbor,
out Vector3 left, out Vector3 right);
+ ///
public readonly bool GetPortalPoints(NavNode node, NavNode neighbor, out Vector3 left, out Vector3 right)
{
CheckValidPtrAndThrow();
@@ -389,6 +414,7 @@ public readonly bool GetPortalPoints(NavNode node, NavNode neighbor, out Vector3
static extern void GetInstanceTransform(IntPtr navMesh, NavNode node,
out Vector3 position, out Quaternion rotation);
+ ///
public readonly void GetInstanceTransform(NavNode node, out Vector3 position, out Quaternion rotation)
{
CheckValidPtrAndThrow();
@@ -400,6 +426,7 @@ public readonly void GetInstanceTransform(NavNode node, out Vector3 position, ou
[NativeName("DecodePolyIdType")]
static extern int GetNodeTypeInternal(NavNode node);
+ ///
public readonly NavNodeType GetNodeType(NavNode node)
{
CheckValidPtrAndThrow();
@@ -414,6 +441,7 @@ public readonly NavNodeType GetNodeType(NavNode node)
[NativeName("GetLinkPolyRef")]
static extern NavNode GetLinkNode(int linkInstance);
+ ///
public readonly NavNode GetLinkNode(NavMeshLinkInstance linkInstance)
{
CheckValidPtrAndThrow();
@@ -425,6 +453,7 @@ public readonly NavNode GetLinkNode(NavMeshLinkInstance linkInstance)
static extern unsafe NavQueryStatus Raycast(IntPtr navMeshQuery, NavLocation start, Vector3 targetPosition,
int areaMask, void* costs, out NavMeshHit hit, void* path, out int pathCount, int maxPath);
+ ///
public readonly unsafe NavQueryStatus Raycast(out NavMeshHit hit, NavLocation start, Vector3 targetPosition,
int areaMask = NavMesh.AllAreas, NativeArray costs = new())
{
@@ -444,6 +473,7 @@ public readonly unsafe NavQueryStatus Raycast(out NavMeshHit hit, NavLocation st
return status;
}
+ ///
public readonly unsafe NavQueryStatus Raycast(out NavMeshHit hit, NativeSlice path, out int pathCount,
NavLocation start, Vector3 targetPosition,
int areaMask = NavMesh.AllAreas, NativeArray costs = new())
@@ -472,6 +502,7 @@ static extern unsafe NavQueryStatus GetEdgesAndNeighbors(IntPtr navMeshPtr, NavN
void* vertices, void* neighbors, void* edgeIndices,
out int vertCount, out int neighborsCount);
+ ///
public readonly unsafe NavQueryStatus GetEdgesAndNeighbors(NavNode node,
NativeSlice edgeVertices, NativeSlice neighbors, NativeSlice edgeIndices,
out int verticesCount, out int neighborsCount)
@@ -501,6 +532,7 @@ public readonly unsafe NavQueryStatus GetEdgesAndNeighbors(NavNode node,
static extern unsafe int GetGeneratedLinkNodes(int navMeshDataInstance, void* nodes, int nodesLength,
int start, int size);
+ ///
public readonly unsafe int GetGeneratedLinkNodes(NavMeshDataInstance navMeshInstance,
NativeSlice linkNodes, int start = 0, int length = int.MaxValue)
{
@@ -515,6 +547,7 @@ public readonly unsafe int GetGeneratedLinkNodes(NavMeshDataInstance navMeshInst
[NativeName("GetGeneratedLinksCount")]
static extern int GetGeneratedLinksCountInternal(int navMeshDataInstanceId);
+ ///
public readonly int GetGeneratedLinksCount(NavMeshDataInstance navMeshInstance)
{
CheckValidPtrAndThrow();
diff --git a/Modules/AI/NavMesh/NavMesh.bindings.cs b/Modules/AI/NavMesh/NavMesh.bindings.cs
index d8f3828b70..95ff25ffa0 100644
--- a/Modules/AI/NavMesh/NavMesh.bindings.cs
+++ b/Modules/AI/NavMesh/NavMesh.bindings.cs
@@ -3,6 +3,7 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Bindings;
using UnityEngine.Scripting;
using UnityEngine.Scripting.APIUpdating;
@@ -10,7 +11,22 @@
namespace UnityEngine.AI
{
// Keep this struct in sync with the one defined in "NavMeshBindingTypes.h"
- // Result information for NavMesh queries.
+ ///Information about a position that is the result of a query ran on the NavMesh.
+ ///
+ /// The object represents a valid result if the distance and position properties have finite values. Otherwise, the object represents a result that could not be calculated from the input data provided to the query. Refer to the documentation of each query method for details about the situations that produce invalid results.
+ ///
+ ///**Note:** You can use float.isFinite() to determine whether a value is finite.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
[MovedFrom("UnityEngine")]
public struct NavMeshHit
{
@@ -20,32 +36,60 @@ public struct NavMeshHit
int m_Mask;
int m_Hit;
- // Position of hit.
+ ///Position of hit.
+ ///It is a position that a **NavMesh Agent** can move to, if it has a agentTypeID value that matches the agentTypeID of the NavMesh at that position. The position lies inside a NavMesh polygon. When the NavMesh also contains HeightMesh data, the position aligns to the HeightMesh polygon that is closest on the vertical axis.
+ ///
+ ///If the position coordinates are not finite, the entire NavMeshHit object represents the result of an invalid query.
+ ///
+ ///
+ ///
public Vector3 position { get => m_Position; set => m_Position = value; }
- // Normal at the point of hit.
+ ///Normal of the polygon edge where the query terminates.
+ ///The vector points towards the inner side of the last NavMesh polygon that the query traverses.
+ ///
+ ///If the query terminates inside a polygon, and is therefore not blocked by an edge, the normal is .
+ ///
+ ///**Note:** None of the query methods returns the normal of the polygon itself.
public Vector3 normal { get => m_Normal; set => m_Normal = value; }
- // Distance to the point of hit.
+ ///Distance to the point of hit.
+ ///If the value is not finite, the entire NavMeshHit object represents the result of an invalid query.
public float distance { get => m_Distance; set => m_Distance = value; }
- // Mask specifying NavMesh area index at point of hit.
+ ///Bitmask that specifies the NavMesh area type at the point of hit.
+ ///The index at which the binary representation of the integer contains a bit turned on is the number of the area type.
+ ///
+ /// If the query proceeds uninterrupted to the target position, the mask represents the area type of the NavMesh polygon where the resulting lies.
+ ///
+ /// When the query terminates at the edge of a NavMesh polygon that is of a different type than the ones allowed by the input parameters, the mask represents the area type of the polygon that blocks the query.
+ ///
+ /// When the query terminates at an edge of the NavMesh the mask is 0, to signify that there is no polygon beyond that position.
public int mask { get => m_Mask; set => m_Mask = value; }
- // Flag set when hit.
+ ///Flag set when the query encounters a particular valid situation.
+ ///The queries set this flag differently. reports hit as true every time it returns a valid position on the NavMesh. The rest of the methods report hit as true when the edge of a NavMesh polygon blocks the query before it can reach the target position. In all other cases hit is false.
public bool hit { get => m_Hit != 0; set => m_Hit = value ? 1 : 0; }
}
// Keep this struct in sync with the one defined in "NavMeshBindingTypes.h"
- // Contains data describing a triangulation of the navmesh
+ ///Contains data describing a triangulation of a navmesh.
[UsedByNativeCode]
[MovedFrom("UnityEngine")]
public struct NavMeshTriangulation
{
+ ///Vertices for the navmesh triangulation.
+ ///Vertices are referenced by the indices.
public Vector3[] vertices;
+ ///Triangle indices for the navmesh triangulation.
+ ///Contains 3 integers for each triangle. These integers refer to the vertices array.
public int[] indices;
+ ///NavMesh area indices for the navmesh triangulation.
+ ///Contains one element for each triangle.
public int[] areas;
+ ///NavMeshLayer values for the navmesh triangulation.
+ ///Contains one element for each triangle.
[Obsolete("Use areas instead.")]
public int[] layers => areas;
}
@@ -53,14 +97,20 @@ public struct NavMeshTriangulation
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Stub class for NavMeshData passing
+ ///Contains and represents NavMesh data.
+ ///An object of this class can be used for creating instances of NavMeshes. See . The contained NavMesh can be built and updated using the build API. See and methods therein.
[NativeHeader("Modules/AI/NavMesh/NavMesh.bindings.h")]
public sealed class NavMeshData : Object
{
+ ///Constructs a new object for representing a NavMesh for the default agent type.
+ ///At construction this NavMesh is empty, i.e. there are no polygons. You can use this class to create, build and add a NavMesh at runtime.
public NavMeshData()
{
Internal_Create(this, 0);
}
+ ///Constructs a new object representing a NavMesh for the specified agent type.
+ /// The agent type ID to create a NavMesh for.
public NavMeshData(int agentTypeID)
{
Internal_Create(this, agentTypeID);
@@ -69,24 +119,41 @@ public NavMeshData(int agentTypeID)
[StaticAccessor("NavMeshDataBindings", StaticAccessorType.DoubleColon)]
static extern void Internal_Create([Writable] NavMeshData mono, int agentTypeID);
+ ///Returns the bounding volume of the input geometry used to build this NavMesh (RO).
+ ///If the NavMesh data has not been built, the bounds will have zero values.
public extern Bounds sourceBounds { get; }
+ ///Gets or sets the world space position of the NavMesh data.
+ ///The default value is zero - that is, the world space origin.
public extern Vector3 position { get; set; }
+ ///Gets or sets the orientation of the NavMesh data.
+ ///The default value is - that is, the NavMesh up axis is the same as the world space y-axis.
public extern Quaternion rotation { get; set; }
internal extern bool hasHeightMeshData { [NativeMethod("HasHeightMeshData")] get; }
internal extern NavMeshBuildSettings buildSettings { get; }
}
+ ///The instance is returned when adding NavMesh data.
+ ///A valid NavMesh data instance is available to the navigation system. This means you can calculate paths etc. using that instance. You also need the instance if you want to remove the NavMesh data at a later time.
+ ///
+ ///
+ ///
public struct NavMeshDataInstance
{
+ ///True if the NavMesh data is added to the navigation system - otherwise false (RO).
public bool valid => id != 0 && NavMesh.IsValidNavMeshDataHandle(id);
internal int id { get; set; }
+ ///Removes this instance from the NavMesh system.
+ ///An identical but convenient alternative to calling . If the instance is not valid, e.g. has been removed before, the call has no effect.
public void Remove()
{
NavMesh.RemoveNavMeshDataInternal(id);
}
+ ///Get or set the owning Object.
+ ///If the instance is invalid: setting the owner has no effect and getting it will return null.
+ ///
public Object owner
{
get => NavMesh.InternalGetOwner(id);
@@ -109,6 +176,8 @@ internal void FlagAsInSelectionHierarchy()
}
// Keep this struct in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Used for runtime manipulation of links connecting polygons of the NavMesh.
+ ///A typical use case is to connect different navigation meshes. Use the method to instantiate a link with these properties in the navigation system. The NavMesh Link component creates its runtime data in this way.
public struct NavMeshLinkData
{
Vector3 m_StartPosition;
@@ -119,29 +188,59 @@ public struct NavMeshLinkData
int m_Area;
int m_AgentTypeID;
+ ///Start position of the link.
+ ///If the is positive, this position specifies the midpoint of the starting edge.
public Vector3 startPosition { get => m_StartPosition; set => m_StartPosition = value; }
+ ///End position of the link.
+ ///If the is positive, this position specifies the midpoint of the ending edge.
public Vector3 endPosition { get => m_EndPosition; set => m_EndPosition = value; }
+ ///If positive, overrides the pathfinder cost to traverse the link.
+ ///When searching for a path this cost multiplies the Euclidean distance between the link end points when scoring the link. If the value is negative, the default cost based on area type is used. The value must be >= 1.0.
public float costModifier { get => m_CostModifier; set => m_CostModifier = value; }
+ ///If true, the link can be traversed in both directions, otherwise only from start to end position.
public bool bidirectional { get => m_Bidirectional != 0; set => m_Bidirectional = value ? 1 : 0; }
+ ///If positive, the link will be rectangle aligned along the line from start to end.
+ ///This allows paths to enter the link at any location along the end sides. If not positive, the link endpoints will be represented as points.
public float width { get => m_Width; set => m_Width = value; }
+ ///Area type of the link.
+ ///Areas And Costs
public int area { get => m_Area; set => m_Area = value; }
+ ///Specifies which agent type this link is available for.
public int agentTypeID { get => m_AgentTypeID; set => m_AgentTypeID = value; }
}
+ ///Represents a link available for pathfinding.
+ ///You obtain a valid object when you call to create one specific link in the navigation system. Conversely, you need to pass it into to remove that instance of the link from the system. Use this object to check or modify the state of the link instance by calling the following methods: , , , , and .
+ ///
+ ///
+ ///
+ /// Empty objects created when you instantiate this struct do not represent any link that exists in the navigation system. The and methods return a value of false for objects created in this manner.
public partial struct NavMeshLinkInstance
{
internal int id { get; set; }
}
+ ///Specifies which agent type and areas to consider when searching the NavMesh.
+ ///This struct is used with the NavMesh query methods overloaded with the query filter argument.
+ ///
+ ///
+ ///
+ ///
public struct NavMeshQueryFilter
{
const int k_AreaCostElementCount = 32;
internal float[] costs { get; private set; }
+ ///A bitmask representing the traversable area types.
public int areaMask { get; set; }
+ ///The agent type ID, specifying which navigation meshes to consider for the query functions.
public int agentTypeID { get; set; }
+ ///Returns the area cost multiplier for the given area type for this filter.
+ ///The default value is 1.
+ /// Index to retrieve the cost for.
+ ///The cost multiplier for the supplied area index.
public float GetAreaCost(int areaIndex)
{
if (costs == null)
@@ -156,6 +255,10 @@ public float GetAreaCost(int areaIndex)
return costs[areaIndex];
}
+ ///Sets the pathfinding cost multiplier for this filter for a given area type.
+ ///Calling SetAreaCost the first time on a NavMeshQueryFilter object causes an internal allocation of the maximum 32 cost modifiers.
+ /// The area index to set the cost for.
+ /// The cost for the supplied area index.
public void SetAreaCost(int areaIndex, float cost)
{
if (costs == null)
@@ -168,15 +271,63 @@ public void SetAreaCost(int areaIndex, float cost)
}
}
+ ///Singleton class to access the baked NavMesh.
+ ///Use the NavMesh class to perform spatial queries such as pathfinding and walkability tests. This class also lets you set the pathfinding cost for specific area types, and tweak the global behavior of pathfinding and avoidance.
+ ///
+ ///Before you can use spatial queries, you must first bake the NavMesh to your scene.
+ ///
+ ///See also:
+ ///
+ ///• <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/CreateNavMesh.html">Create a NavMesh</a> – for more information on how to setup and bake NavMesh
+ ///• <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/AreasAndCosts.html">Areas and Costs</a> – to learn how to use different Area types.
+ ///• – to learn how to control and move NavMesh Agents.
+ ///• – to learn how to control NavMesh Obstacles using scripting.
+ ///• <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> – to learn how to control Off-Mesh Links using scripting.
[NativeHeader("Modules/AI/NavMeshManager.h")]
[NativeHeader("Modules/AI/NavMesh/NavMesh.bindings.h")]
[StaticAccessor("NavMeshBindings", StaticAccessorType.DoubleColon)]
[MovedFrom("UnityEngine")]
- public static class NavMesh
+ public static partial class NavMesh
{
+ ///Area mask constant that includes all NavMesh areas.
+ ///
+ /// Use the mask in query functions, such as , to indicate that all NavMesh area types are accepted.
+ ///
+ /// See Areas and Costs to learn how to use different Area types.
+ ///
+ ///
+ ///
+ ///
public const int AllAreas = ~0;
+ ///Registers callback methods to be invoked before the NavMesh system updates.
+ ///This is useful for updating the NavMesh and links before the agents are simulated during the update cycle.
+ ///
public delegate void OnNavMeshPreUpdate();
+ ///Set a function to be called before the NavMesh is updated during the frame update execution.
+ ///This lets you set a delegate function to be called every frame, right before the NavMesh system gets updated.
+ ///
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered pre-update callbacks
public static OnNavMeshPreUpdate onPreUpdate;
[RequiredByNativeCode]
@@ -192,10 +343,88 @@ static void Internal_CallPreUpdateListeners()
onPreUpdate();
}
- // Trace a ray between two points on the NavMesh.
+ ///Trace a line between two points on the NavMesh.
+ ///
+ /// The source and destination points are first mapped on the NavMesh, then a ray is traced from the source point towards the target. If the ray hits a NavMesh boundary, the function returns true and the hit data is filled. If the path from the source to target is unobstructed, the function returns false.
+ ///
+ ///If the raycast terminates on an outer edge, hit.mask is 0; otherwise it contains the area mask of the blocking polygon.
+ ///
+ ///This function can be used to check if an agent can walk unobstructed between two points on the NavMesh. For example if your character has an evasive dodge move which needs space, you can shoot a ray from the characters location to multiple directions to find a spot where the character can dodge to.
+ ///
+ ///The Raycast is different from physics ray cast because it works on “2.5D”, on the NavMesh. The difference to physics ray casts is that NavMesh ray casts can detect all kinds of navigation obstructions, such as holes in the ground, and it can also climb up slopes, if the area is navigable.
+ /// If you want to find the nearest point on the NavMesh, use physics ray cast to find a point in the world. For more information, refer to the Move an Agent to a Position Clicked by the Mouse example.
+ ///
+ /// The origin of the ray.
+ /// The end of the ray.
+ /// Holds the properties of the ray cast resulting location.
+ /// A bitfield mask specifying which NavMesh areas can be passed when tracing the ray.
+ ///True if the ray is terminated before reaching target position. Otherwise returns false.
+ ///
+ ///
+ ///
public static extern bool Raycast(Vector3 sourcePosition, Vector3 targetPosition, out NavMeshHit hit, int areaMask);
- // Calculate a path between two points and store the resulting path.
+ ///Calculate a path between two points and store the resulting path.
+ ///Use this function to avoid gameplay delays by planning a path before it is needed. You can also use this function to check if a target position is reachable before moving the agent.
+ ///
+ ///This function is synchronous. It performs path finding immediately which can adversely affect the frame rate when processing very long paths. It is recommended to only perform a few path finds per frame when, for example, evaluating distances to cover points.
+ ///
+ ///Use the returned path to set the path for an agent with . For SetPath to work, the agent must be close to the starting point.
+ /// The initial position of the path requested.
+ /// The final position of the path requested.
+ /// A bitfield mask specifying which NavMesh areas can be passed when calculating a path.
+ /// The resulting path.
+ ///True if either a complete or partial path is found. False otherwise.
+ ///
+ /// 1.0f)
+ /// {
+ /// elapsed -= 1.0f;
+ /// NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, path);
+ /// }
+ /// for (int i = 0; i < path.corners.Length - 1; i++)
+ /// Debug.DrawLine(path.corners[i], path.corners[i + 1], Color.red);
+ /// }
+ ///}
+ ///]]>
+ ///
public static bool CalculatePath(Vector3 sourcePosition, Vector3 targetPosition, int areaMask, NavMeshPath path)
{
path.ClearCorners();
@@ -204,46 +433,206 @@ public static bool CalculatePath(Vector3 sourcePosition, Vector3 targetPosition,
static extern bool CalculatePathInternal(Vector3 sourcePosition, Vector3 targetPosition, int areaMask, NavMeshPath path);
- // Locate the closest NavMesh edge from a point on the NavMesh.
+ ///Locate the closest NavMesh edge from a point on the NavMesh.
+ ///The returned object contains the position
+ ///and details of the nearest point on the nearest edge of the
+ ///navmesh. This can be used to query how much extra space there is around the agent.
+ /// The origin of the distance query.
+ /// Holds the properties of the resulting location.
+ /// A bitfield mask specifying which NavMesh areas can be passed when finding the nearest edge.
+ ///True if the nearest edge is found.
+ ///
+ ///
+ ///
public static extern bool FindClosestEdge(Vector3 sourcePosition, out NavMeshHit hit, int areaMask);
- // Sample the NavMesh closest to the point specified.
+ ///Finds the nearest point based on the NavMesh within a specified range.
+ ///The nearest point is found by projecting the input point onto nearby NavMesh instances along the vertical axis. This vertical axis has been chosen for each instance at the time of creation . If this step does not find a projected point within the specified distance, then sampling is extended to surrounding NavMesh positions.
+ ///
+ ///Finds the nearest point based on the distance to the query point. This function does not consider obstructions. For example, in a two-story structure, if the sourcePosition is set to a point on the ceiling on the first floor, the nearest point might be found on the second floor rather than the first floor. The ceiling is not considered as an obstruction.
+ ///
+ ///This function may reduce the frame rate if a large search radius is specified. To avoid frame rate issues, it is recommended that you specify a maxDistance of twice the agent height.
+ ///
+ ///If you are trying to find a random point on the NavMesh, you should use the recommended radius and perform the find multiple times instead of using a very large radius.
+ /// The origin of the sample query.
+ /// Holds the properties of the resulting location. The value of hit.normal is never computed. It is always (0,0,0).
+ /// Sample within this distance from sourcePosition.
+ /// A mask that specifies the NavMesh areas allowed when finding the nearest point.
+ ///True if the nearest point is found.
+ ///
+ ///
+ ///
public static extern bool SamplePosition(Vector3 sourcePosition, out NavMeshHit hit, float maxDistance, int areaMask);
+ ///Sets the cost for traversing over geometry of the layer type on all agents.
+ ///This will replace any custom layer costs on all agents.
[Obsolete("Use SetAreaCost instead.")]
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("SetAreaCost")]
public static extern void SetLayerCost(int layer, float cost);
+ ///Gets the cost for traversing over geometry of the layer type on all agents.
[Obsolete("Use GetAreaCost instead.")]
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("GetAreaCost")]
public static extern float GetLayerCost(int layer);
+ ///Returns the layer index for a named layer.
+ ///If the named layer does not exist returns -1.
[Obsolete("Use GetAreaFromName instead.")]
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("GetAreaFromName")]
public static extern int GetNavMeshLayerFromName(string layerName);
+ ///Sets the cost for finding path over geometry of the area type on all agents.
+ ///
+ /// This will replace any custom area costs on all agents, and set the default cost for new agents that are created after calling the function. The cost must be larger than 1.0.
+ ///
+ ///Use to find the area index based on the name of the NavMesh area type.
+ ///
+ ///
+ /// Index of the area to set.
+ /// New cost.
+ ///
+ ///
+ ///
+ ///Areas and Costs
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("SetAreaCost")]
public static extern void SetAreaCost(int areaIndex, float cost);
+ ///Gets the cost for path finding over geometry of the area type.
+ ///The value applies to all agents unless the value has been customized per agent by calling .
+ ///
+ ///Use to find the area index based on the name of the area type.
+ /// Index of the area to get.
+ ///Areas and Costs
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("GetAreaCost")]
public static extern float GetAreaCost(int areaIndex);
+ ///Returns the area index for a named NavMesh area type.
+ /// Name of the area to look up.
+ ///Index if the specified area name exists, or -1 if no area type has the specified name.
+ ///
+ ///
+ ///
+ ///Areas and Costs
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("GetAreaFromName")]
public static extern int GetAreaFromName(string areaName);
+ ///Get all the NavMesh area names.
+ ///Names of all the NavMesh areas.
[StaticAccessor("GetNavMeshProjectSettings()")]
[NativeName("GetAreaNames")]
public static extern string[] GetAreaNames();
+ ///Calculates a triangulation of all the NavMeshes that are present in the scene at the time of the call.
+ ///Calculates and returns a simple triangulation of all the NavMeshes that are currently active. The resulting object contains vertices, triangle indices and NavMesh area types . The triangles from each NavMesh instance are grouped together in the array. These triangle groups are further sorted in the array based on the agent types that their originating NavMeshes were built for.
+ ///
+ ///The triangulation captures the current shape of the NavMeshes, which can include temporary holes carved by NavMeshObstacles.
+ ///
+ ///
+ ///The returned mesh contains only the triangles used for pathfinding. It does not contain the detail that is used to place the agents on the walkable surface. This is noticeable on locations with curved surfaces.
+ ///Object that contains a list of vertices and a list of indices that describe the triangles of the active NavMeshes.
+ ///Areas and Costs
public static extern NavMeshTriangulation CalculateTriangulation();
- //*undocumented* DEPRECATED
+ ///
[Obsolete("use NavMesh.CalculateTriangulation() instead.")]
public static void Triangulate(out Vector3[] vertices, out int[] indices)
{
@@ -252,20 +641,38 @@ public static void Triangulate(out Vector3[] vertices, out int[] indices)
indices = results.indices;
}
+ ///
[Obsolete("AddOffMeshLinks has no effect and is deprecated.")]
public static void AddOffMeshLinks() {}
+ ///
[Obsolete("RestoreNavMesh has no effect and is deprecated.")]
public static void RestoreNavMesh() {}
+ ///Describes how far in the future the agents predict collisions for avoidance.
+ ///The larger the value, the earlier the agents will start to avoid each other if they are on collision course. The value is measured in seconds. Default value is 2.0, a good range for tuning is between 0.5 and 5.0.
[StaticAccessor("GetNavMeshManager()")]
public static extern float avoidancePredictionTime { get; set; }
+ ///The maximum number of nodes processed for each frame during the asynchronous pathfinding process.
+ ///During the pathfinding process, the pathfinder expands only a certain number of nodes (NavMesh polygons) for each frame. This allows for smoother gameplay when processing long paths or when processing a large number of requests concurrently. However, the path request might take many frames to process.
+ ///
+ ///The iteration count only affects asynchronous pathfinding. This method of pathfinding is used when the NavMesh Agent destination is set with or .
+ ///
+ ///Increasing this value causes faster path processing but it might also cause frame rate issues. The default value is 100. An ideal value is between 50 and 500.
[StaticAccessor("GetNavMeshManager()")]
public static extern int pathfindingIterationsPerFrame { get; set; }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ///Adds the specified NavMeshData to the game.
+ ///This makes the NavMesh data available for agents and NavMesh queries. Returns an instance for later removing the NavMesh data from the runtime.
+ ///
+ ///The instance returned will be valid unless the NavMesh data could not be added - e.g. due to running out of memory or navmesh data being loaded from a corrupted file.
+ /// Contains the data for the navmesh.
+ ///Representing the added navmesh.
+ ///
+ ///
public static NavMeshDataInstance AddNavMeshData(NavMeshData navMeshData)
{
if (navMeshData == null) throw new ArgumentNullException(nameof(navMeshData));
@@ -275,6 +682,39 @@ public static NavMeshDataInstance AddNavMeshData(NavMeshData navMeshData)
return handle;
}
+ ///Adds the specified NavMeshData to the game.
+ ///This function is similar to above, but the position and rotation specified is applied in addition to the position and rotation where the NavMesh data was baked.
+ /// Contains the data for the navmesh.
+ /// Translate the navmesh to this position.
+ /// Rotate the navmesh to this orientation.
+ ///Representing the added navmesh.
+ ///
+ ///
+ ///
public static NavMeshDataInstance AddNavMeshData(NavMeshData navMeshData, Vector3 position, Quaternion rotation)
{
if (navMeshData == null) throw new ArgumentNullException(nameof(navMeshData));
@@ -284,6 +724,11 @@ public static NavMeshDataInstance AddNavMeshData(NavMeshData navMeshData, Vector
return handle;
}
+ ///Removes the specified from the game, making it unavailable for agents and queries.
+ ///Use the instance returned by to remove the corresponding NavMesh data. If the instance is not valid, e.g. has been removed before, the call has no effect.
+ /// The instance of a NavMesh to remove.
+ ///
+ ///
public static void RemoveNavMeshData(NavMeshDataInstance handle)
{
RemoveNavMeshDataInternal(handle.id);
@@ -320,6 +765,16 @@ public static void RemoveNavMeshData(NavMeshDataInstance handle)
[NativeName("UnloadData")]
internal static extern void RemoveNavMeshDataInternal(int handle);
+ ///Adds a link to the NavMesh. The link is described by the NavMeshLinkData struct.
+ ///Returns an instance identifier for the added link.
+ ///
+ ///The returned instance is valid if the link was successfully added. The instance can be used to later remove the link using RemoveLink().
+ ///
+ ///**Note:** If the area is set to Not Walkable, or if adding a link would exceed the maximum number of active links (65535) the link will fail to be added – and the valid property will be false.
+ /// Object that describes the properties of the link.
+ ///Object that identifies the added link.
+ ///
+ ///
public static NavMeshLinkInstance AddLink(NavMeshLinkData link)
{
var handle = new NavMeshLinkInstance();
@@ -327,6 +782,16 @@ public static NavMeshLinkInstance AddLink(NavMeshLinkData link)
return handle;
}
+ ///Adds a link to the NavMesh. The link is described by the NavMeshLinkData struct.
+ ///Returns an instance identifier for the added link.
+ ///
+ ///This function is similar to AddLink above, but the position and rotation specified is applied to the start and end positions of the link. The rotation also specifies the local up-axis of the link.
+ /// Object that describes the properties of the link.
+ /// Translate the link to this position.
+ /// Rotate the link to this orientation.
+ ///Object that identifies the added link.
+ ///
+ ///
public static NavMeshLinkInstance AddLink(NavMeshLinkData link, Vector3 position, Quaternion rotation)
{
var handle = new NavMeshLinkInstance();
@@ -334,36 +799,92 @@ public static NavMeshLinkInstance AddLink(NavMeshLinkData link, Vector3 position
return handle;
}
+ ///Removes a link from the NavMesh.
+ ///Use the instance returned by to remove the corresponding link.
+ /// The instance of a link to remove.
+ ///
public static void RemoveLink(NavMeshLinkInstance handle)
{
RemoveLinkInternal(handle.id);
}
+ ///Determines whether the instance of the link can be used to calculate paths, and if NavMesh agents can move over it.
+ ///Use this method to determine if paths for the assigned agent type can traverse this link or not.
+ ///
+ ///A link instance is active by default regardless of whether the ends connect to NavMesh surfaces or not. To change the link's state, call . After you remove the link from the running navigation system this method always returns false .
+ ///
+ ///
+ ///
+ ///This method is available as of 2023.2.
+ /// The link instance whose state to query.
+ ///True if agents can plan paths through, and traverse, this instance of the link, otherwise false.
+ ///
public static bool IsLinkActive(NavMeshLinkInstance handle)
{
return IsOffMeshConnectionActive(handle.id);
}
+ ///Activates or deactivates the link instance. An active link instance can be traversed by agents and used to plan paths, but a deactivated link cannot.
+ ///This method changes the state of the link instance immediately. Any path that you calculate afterwards takes into account the new state of the link. When you disable the link instance any paths that have already been calculated through it get a status value of invalid .
+ ///
+ ///You can call this method at any time to deactivate the link and prevent agents from moving through a section of the game level, for example through a door that connects two rooms. Conversely, you can activate the link and allow the agents to move between the respective game level sections.
+ ///
+ ///Deactivated links remain connected to the NavMesh surfaces and they do not need to find the connection points again when they are reactivated.
+ ///
+ ///Any link instance created with the method is active by default.
+ ///
+ ///
+ ///
+ ///This method is available as of 2023.2.
+ /// The link instance whose active state to modify.
+ /// Whether agents can plan paths through, and traverse, the link. When the value is true, agents can plan paths through, and traverse, the link. Otherwise, no paths can use the link instance.
+ ///
public static void SetLinkActive(NavMeshLinkInstance handle, bool value)
{
SetOffMeshConnectionActive(handle.id, value);
}
+ ///Determines whether or not a NavMesh agent is currently using this link.
+ ///Use this method to determine if your NavMesh agent can move onto the specified NavMesh link instance. Only one NavMesh agent can traverse a NavMesh link instance at any one time, so your agent can't move onto a NavMesh link instance that is already occupied. A NavMesh link instance is occupied when any NavMesh agent moves onto the link as part of the path the agent has calculated to the destination . When the agent moves off of the link, either automatically or through a call to , the link instance is no longer occupied.
+ ///
+ ///This method is available as of 2023.2.
+ /// The link instance whose state to query.
+ ///True if an agent is currently traversing the link, otherwise false.
+ ///
public static bool IsLinkOccupied(NavMeshLinkInstance handle)
{
return IsOffMeshConnectionOccupied(handle.id);
}
+ ///Determines whether the link instance is part of the current data used for navigation.
+ /// The identifier of the link instance to check.
+ ///True if the NavMesh link is added to the navigation system - otherwise false.
public static bool IsLinkValid(NavMeshLinkInstance handle)
{
return IsValidLinkHandle(handle.id);
}
+ ///Gets the object, if any, that is associated with the link instance.
+ ///Use this method to obtain a reference to the component that created the link, or more generally, to any object that contains useful information about this specific link that is active in the navigation system. We refer to that object as the "owner". The owner is null for any new link instance created with . Therefore you need to first call in order to retrieve the same object later. This "owner" is also referenced by the property when you query for the next link on a NavMesh agent's path.
+ ///
+ ///When the link instance is removed the owner property returns null once again.
+ /// The identifier of the link instance whose owner needs to be retrieved.
+ ///The object that was passed into for the specified link instance.
+ ///
+ ///Returns null when no owner object has been assigned or when the link instance is not valid.
+ ///
public static Object GetLinkOwner(NavMeshLinkInstance handle)
{
return InternalGetLinkOwner(handle.id);
}
+ ///Associates an object with the instance of a link.
+ ///Call to retrieve a reference to the assigned object. The property obtained from the path of an agent also points to the object that has been assigned to the link.
+ ///
+ ///If the instance of the link is not valid, setting the owner has no effect and getting it returns null.
+ /// The identifier of the link instance for which you assign an owner.
+ /// An object that carries useful information in relation to the instance of the link.
+ ///
public static void SetLinkOwner(NavMeshLinkInstance handle, Object owner)
{
var ownerID = owner != null ? owner.GetEntityId() : EntityId.None;
@@ -388,6 +909,14 @@ public static void SetLinkOwner(NavMeshLinkInstance handle, Object owner)
[StaticAccessor("GetNavMeshManager()")]
internal static extern void SetOffMeshConnectionActive(int linkHandle, bool activated);
+ ///Samples the position nearest the sourcePosition on any NavMesh built for the agent type specified by the filter.
+ ///Consider only positions on areas defined in the .
+ ///A maximum search radius is set by maxDistance. The information of any found position is returned in the hit argument.
+ /// The origin of the sample query.
+ /// Holds the properties of the resulting location. The value of hit.normal is never computed. It is always (0,0,0).
+ /// Sample within this distance from sourcePosition.
+ /// A filter specifying which NavMesh areas are allowed when finding the nearest point.
+ ///True if the nearest point is found.
public static bool SamplePosition(Vector3 sourcePosition, out NavMeshHit hit, float maxDistance, NavMeshQueryFilter filter)
{
return SamplePositionFilter(sourcePosition, out hit, maxDistance, filter.agentTypeID, filter.areaMask);
@@ -396,6 +925,12 @@ public static bool SamplePosition(Vector3 sourcePosition, out NavMeshHit hit, fl
// a CUSTOM "SamplePosition" exists elsewhere. We need to pick unique name here to compile generated code in batch-builds
static extern bool SamplePositionFilter(Vector3 sourcePosition, out NavMeshHit hit, float maxDistance, int type, int mask);
+ ///Locate the closest NavMesh edge from a point on the NavMesh, subject to the constraints of the filter argument.
+ ///The returned NavMeshHit object contains the position and details of the nearest point on the nearest edge of the NavMesh. This can be used to query how much extra space there is around the agent.
+ /// The origin of the distance query.
+ /// Holds the properties of the resulting location.
+ /// A filter specifying which NavMesh areas can be passed when finding the nearest edge.
+ ///True if the nearest edge is found.
public static bool FindClosestEdge(Vector3 sourcePosition, out NavMeshHit hit, NavMeshQueryFilter filter)
{
return FindClosestEdgeFilter(sourcePosition, out hit, filter.agentTypeID, filter.areaMask);
@@ -404,6 +939,13 @@ public static bool FindClosestEdge(Vector3 sourcePosition, out NavMeshHit hit, N
// a CUSTOM "FindClosestEdge" exists elsewhere. We need to pick unique name here to compile generated code in batch-builds
static extern bool FindClosestEdgeFilter(Vector3 sourcePosition, out NavMeshHit hit, int type, int mask);
+ ///Traces a line between two positions on the NavMesh, subject to the constraints defined by the filter argument.
+ ///The line is terminated on outer edges or a non-passable area.
+ /// The origin of the ray.
+ /// The end of the ray.
+ /// Holds the properties of the ray cast resulting location.
+ /// A filter specifying which NavMesh areas can be passed when tracing the ray.
+ ///True if the ray is terminated before reaching target position. Otherwise returns false.
public static bool Raycast(Vector3 sourcePosition, Vector3 targetPosition, out NavMeshHit hit, NavMeshQueryFilter filter)
{
return RaycastFilter(sourcePosition, targetPosition, out hit, filter.agentTypeID, filter.areaMask);
@@ -412,6 +954,12 @@ public static bool Raycast(Vector3 sourcePosition, Vector3 targetPosition, out N
// a CUSTOM "Raycast" exists elsewhere. We need to pick unique name here to compile generated code in batch-builds
static extern bool RaycastFilter(Vector3 sourcePosition, Vector3 targetPosition, out NavMeshHit hit, int type, int mask);
+ ///Calculates a path between two positions mapped to the NavMesh, subject to the constraints and costs defined by the filter argument.
+ /// The initial position of the path requested.
+ /// The final position of the path requested.
+ /// A filter specifying the cost of NavMesh areas that can be passed when calculating a path.
+ /// The resulting path.
+ ///True if a either a complete or partial path is found and false otherwise.
public static bool CalculatePath(Vector3 sourcePosition, Vector3 targetPosition, NavMeshQueryFilter filter, NavMeshPath path)
{
path.ClearCorners();
@@ -420,24 +968,59 @@ public static bool CalculatePath(Vector3 sourcePosition, Vector3 targetPosition,
static extern bool CalculatePathFilterInternal(Vector3 sourcePosition, Vector3 targetPosition, NavMeshPath path, int type, int mask, float[] costs);
+ ///Creates and returns a new entry of NavMesh build settings available for runtime NavMesh building.
+ ///This is useful for creating and storing settings to use for building NavMeshes for different sized characters.
+ ///
+ ///
+ ///
+ ///The will be positive and unique for the created settings.
+ ///The created settings.
+ ///
[StaticAccessor("GetNavMeshProjectSettings()")]
public static extern NavMeshBuildSettings CreateSettings();
//[StaticAccessor("GetNavMeshProjectSettings()")]
//public static extern void UpdateSettings(NavMeshBuildSettings buildSettings);
+ ///Removes the build settings matching the agent type ID.
+ ///If no matching settings are found or the agentTypeID is the default value 0, nothing is removed.
+ /// The ID of the entry to remove.
[StaticAccessor("GetNavMeshProjectSettings()")]
public static extern void RemoveSettings(int agentTypeID);
+ ///Returns an existing entry of NavMesh build settings.
+ ///If no previously-created settings match the provided agent type ID, the returned struct will have the agentTypeID set to -1. See also: .
+ ///
+ ///**Note:** A default entry will always exist for the agentTypeID being 0.
+ /// The ID to look for.
+ ///The settings found.
public static extern NavMeshBuildSettings GetSettingsByID(int agentTypeID);
+ ///Returns the number of registered NavMesh build settings.
+ ///This will always be at least one available, namely the default setting.
+ ///The number of registered entries.
+ ///
[StaticAccessor("GetNavMeshProjectSettings()")]
public static extern int GetSettingsCount();
+ ///Returns an existing entry of NavMesh build settings by its ordered index.
+ ///If the index is outside the valid range (0, GetSettingsCount-1), the returned NavMeshBuildSettings struct will have the agentTypeID set to -1.
+ /// The index to retrieve from.
+ ///The found settings.
+ ///
+ ///
public static extern NavMeshBuildSettings GetSettingsByIndex(int index);
+ ///Returns the name associated with the NavMesh build settings matching the provided agent type ID.
+ ///If no settings are found, the result is an empty string.
+ /// The ID to look for.
+ ///The name associated with the ID found.
public static extern string GetSettingsNameFromID(int agentTypeID);
+ ///Removes all NavMesh surfaces and links from the game.
+ ///Unloads all surfaces and links that have been loaded from the Scene or added with or and frees all the internal resources associated with the NavMesh.
+ ///
+ ///
[StaticAccessor("GetNavMeshManager()")]
[NativeName("CleanupAfterCarving")]
public static extern void RemoveAllNavMeshData();
diff --git a/Modules/AI/NavMesh/NavMesh.deprecated.cs b/Modules/AI/NavMesh/NavMesh.deprecated.cs
index f72a3fe649..5c0642de85 100644
--- a/Modules/AI/NavMesh/NavMesh.deprecated.cs
+++ b/Modules/AI/NavMesh/NavMesh.deprecated.cs
@@ -8,15 +8,30 @@ namespace UnityEngine.AI;
public partial struct NavMeshLinkInstance
{
+ ///True if the NavMesh link is added to the navigation system - otherwise false (RO).
+ ///
[Obsolete("valid has been deprecated. Use NavMesh.IsLinkValid() instead.")]
public bool valid => NavMesh.IsValidLinkHandle(id);
+ ///Removes this instance from the game.
+ ///This method is an identical but convenient alternative to . If the instance is not valid, e.g. has already been removed, the call has no effect.
+ ///
+ ///
[Obsolete("Remove() has been deprecated. Use NavMesh.RemoveLink() instead.")]
public void Remove()
{
NavMesh.RemoveLinkInternal(id);
}
+ ///Get or set the owning .
+ ///If the instance is not valid, setting the owner has no effect and getting it returns null.
+ ///
+ ///Use this property to reference the component that created the link, or more generally, any object that contains useful information about this specific link active in the navigation system. The owner is null for any new link instance created with . You can, at any time, assign any Object to this property and retrieve that reference later.
+ ///
+ ///When the link instance is removed the owner property returns null once again.
+ ///
+ ///
+ ///
[Obsolete("owner has been deprecated. Use NavMesh.GetLinkOwner() and NavMesh.SetLinkOwner() instead.")]
public Object owner
{
diff --git a/Modules/AI/NavMeshExperimental.bindings.cs b/Modules/AI/NavMeshExperimental.bindings.cs
index cdf403758a..fe3a4ab34b 100644
--- a/Modules/AI/NavMeshExperimental.bindings.cs
+++ b/Modules/AI/NavMeshExperimental.bindings.cs
@@ -13,18 +13,32 @@
namespace UnityEngine.Experimental.AI
{
+ ///Represents a compact identifier for the data of a NavMesh node.
+ ///It is used in operations for pinpointing and getting access to relevant nodes in the NavMesh. Each node can be used by only one type of agent.
+ ///
+ ///This identifier becomes invalid once the node gets removed from the NavMesh, either by completely removing the surface or by modifying the surface in the node's immediate vicinity.
+ ///
+ ///
[Obsolete("The experimental PolygonId struct has been deprecated. Use NavNode instead.")]
public struct PolygonId : IEquatable
{
internal ulong polyRef;
+ ///Returns true if the has been created empty and has never pointed to any node in the NavMesh.
public bool IsNull() { return polyRef == 0; }
+ ///Returns true if two objects refer to the same NavMesh node or if they are both null.
+ ///
public static bool operator==(PolygonId x, PolygonId y) { return x.polyRef == y.polyRef; }
+ ///Returns true if two objects refer to different NavMesh nodes or if only one of them is null.
+ ///
public static bool operator!=(PolygonId x, PolygonId y) { return x.polyRef != y.polyRef; }
+ ///Returns the hash code for use in collections.
public override int GetHashCode() { return polyRef.GetHashCode(); }
+ ///Returns true if two objects refer to the same NavMesh node.
public bool Equals(PolygonId rhs) { return rhs == this; }
+ ///Returns true if two objects refer to the same NavMesh node.
public override bool Equals(object obj)
{
if (obj == null || !(obj is PolygonId))
@@ -34,10 +48,24 @@ public override bool Equals(object obj)
}
}
+ ///A world position that is guaranteed to be on the surface of the NavMesh.
+ ///The NavMeshLocation stores the position on the NavMesh surface together with the of the NavMesh node containing that position. Using NavMeshLocations with operations remove the need to project the desired world position onto the NavMesh at the beginning of each and every operation.
+ ///
+ ///A NavMeshLocation can be invalid in two situations:
+ ///1. When it has been created empty, instead of being the result of a operation.
+ ///2. When the NavMesh has been removed or modified at the indicated position or in its close vicinity.
+ ///
+ ///If a NavMeshLocation is made invalid by a carving the NavMesh in its vicinity the NavMeshLocation returns to a valid state if the is removed. This is because removing a restores the NavMesh to its original form without regenerating it.
+ ///
+ ///
+ ///
[Obsolete("The experimental NavMeshLocation struct has been deprecated. Use NavLocation instead.")]
public struct NavMeshLocation
{
+ ///Unique identifier for the node in the NavMesh to which the world position has been mapped.
+ ///
public PolygonId polygon { get; }
+ ///A world position that sits precisely on the surface of the NavMesh or along its links.
public Vector3 position { get; }
internal NavMeshLocation(Vector3 position, PolygonId polygon)
@@ -73,34 +101,72 @@ internal NavMeshLocation(Vector3 position, PolygonId polygon)
//}
// Keep in sync with the values in NavMeshTypes.h
+ ///Bit flags representing the resulting state of operations.
+ ///The main values are Success , Failure and InProgress . A status will usually have only one of these main flags set. The secondary flags (details) are set when specific issues have been encountered during the operation. StatusDetailMask is a bit mask that can be used to filter out these secondary flags.
+ ///
+ ///**Note:** Issues highlighted by the presence of certain detail flags in certain situations might refer to internal structures outside the control of users, thus they will not always be able to mitigate them by taking the necessary actions in their code. Ways for handling these situations will be made available in the future.
[Obsolete("The experimental PathQueryStatus enum has been deprecated. Use NavQueryStatus instead.")]
[Flags]
public enum PathQueryStatus
{
// High level status.
+ ///The operation has failed.
+ ///Check the status for secondary flags that might provide more details about the issue causing the failure.
Failure = 1 << 31,
+ ///The operation was successful.
Success = 1 << 30,
+ ///The operation is in progress.
InProgress = 1 << 29,
// Detail information for status.
+ ///Bitmask that has 0 set for the Success , Failure and InProgress bits and 1 set for all the other flags.
+ ///It can be used to separate the detail flags from the main status flags.
StatusDetailMask = 0x0ffffff,
- WrongMagic = 1 << 0, // Input data is not recognized.
- WrongVersion = 1 << 1, // Input data is in wrong version.
- OutOfMemory = 1 << 2, // Operation ran out of memory.
- InvalidParam = 1 << 3, // An input parameter was invalid.
- BufferTooSmall = 1 << 4, // Result buffer for the query was too small to store all results.
- OutOfNodes = 1 << 5, // Query ran out of nodes during search.
- PartialResult = 1 << 6 // Query did not reach the end location, returning best guess.
+ ///Data in the NavMesh cannot be recognized and used.
+ WrongMagic = 1 << 0,
+ ///Data in the NavMesh world has a wrong version.
+ WrongVersion = 1 << 1,
+ ///Operation ran out of memory.
+ ///**Known issue, will be fixed:** This flag is not currently reported when memory fails to be allocated because the is created with a pathNodePoolSize value too large. The NavMeshQuery will then be silently defective and might produce a crash.
+ OutOfMemory = 1 << 2,
+ ///A parameter did not contain valid information, useful for carring out the NavMesh query.
+ InvalidParam = 1 << 3,
+ ///The node buffer of the query was too small to store all results.
+ ///Creating a different with a larger pathNodePoolSize parameter might solve the issue.
+ BufferTooSmall = 1 << 4,
+ ///Query ran out of node stack space during a search.
+ ///This happens when the query has visited more nodes than there is room in the . To fix this issue try a larger value for the pathNodePoolSize parameter when creating the .
+ OutOfNodes = 1 << 5,
+ ///Query did not reach the end location, returning best guess.
+ PartialResult = 1 << 6
}
// Flags describing polygon properties. Keep in sync with the enum declared in NavMesh.h
+ ///The types of nodes in the navigation data.
+ ///Navigation data is comprised geometrically of polygons and segments connected together.
+ ///
+ ///NavMeshSurface
+ ///NavMeshLink
+ ///Off-mesh Link
[Obsolete("The experimental NavMeshPolyTypes enum has been deprecated. Use NavNodeType instead.")]
public enum NavMeshPolyTypes
{
- Ground = 0, // Regular ground polygons.
- OffMeshConnection = 1 // Off-mesh connections.
+ ///Type of node in the NavMesh representing one surface polygon.
+ ///NavMeshSurface
+ Ground = 0,
+ ///Type of node in the NavMesh representing a point-to-point connection between two positions on the NavMesh surface.
+ ///NavMeshLink
+ ///Off-mesh Link
+ OffMeshConnection = 1
}
+ ///Assembles together a collection of NavMesh surfaces and links that are used as a whole for performing navigation operations.
+ ///Operations are initialized against one world, can use only the NavMeshes inside that world and are not aware of the existence of any other NavMeshWorld.
+ ///
+ ///Copying this object only produces a new reference to the same NavMesh data, it does not duplicate the data in memory.
+ ///
+ ///**Important note:** Currently only a single NavMesh world can be used and a reference to it can be obtained through the method. In the future, multiple NavMesh worlds will be able to be created and any two of them will be completely isolated from each other.
+ ///
[Obsolete("The experimental NavMeshWorld struct has been deprecated. Use NavWorld instead.")]
[StaticAccessor("NavMeshWorldBindingsExperimental", StaticAccessorType.DoubleColon)]
[NativeHeader("Modules/AI/NavMeshExperimental.bindings.h")]
@@ -109,12 +175,16 @@ public struct NavMeshWorld
{
internal IntPtr world;
+ ///Returns true if the NavMeshWorld has been properly initialized.
+ ///Currently the only way to obtain the single possible valid NavMesh world is through a call to .
public bool IsValid()
{
return world != IntPtr.Zero;
}
static extern NavMeshWorld GetDefaultWorldExp();
+ ///Returns a reference to the single that can currently exist and be used in Unity.
+ ///The returned world comprises of all the NavMeshes and connections that are also used through the -related structures.
public static NavMeshWorld GetDefaultWorld()
{
return GetDefaultWorldExp();
@@ -122,6 +192,11 @@ public static NavMeshWorld GetDefaultWorld()
static extern void AddDependencyInternalExp(IntPtr navmesh, JobHandle handle);
+ ///Tells the NavMesh world to halt any changes until the specified job is completed.
+ ///When jobs process operations, it is essential that the NavMesh data does not change. Thus, every time a job of that type is scheduled its must be passed to the NavMeshWorld using this method. Otherwise, an exception will be thrown when the project is running in the Editor.
+ /// The job that needs to be completed before the NavMesh world can be modified in any way.
+ ///
+ ///
public void AddDependency(JobHandle job)
{
if (!IsValid())
@@ -130,6 +205,14 @@ public void AddDependency(JobHandle job)
}
}
+ ///Object used for doing navigation operations in a .
+ ///NavMeshQuery operations can be executed inside jobs ( , ), as opposed to the operations in the -related structures.
+ ///
+ ///To obtain a path between two locations on the NavMesh, you must create a NavMeshQuery with a pathNodePoolSize value in the range from 1 to 65,535. After creating a NavMeshQuery, you must call the following methods in this order: BeginFindPath , UpdateFindPath (can be repeated), EndFindPath , GetPathResult . These methods store state data within the NavMeshQuery. Other methods can be called in any order since they do not change state data.
+ ///
+ ///All methods throw exceptions if any of their parameters are not valid when executed in the Editor.
+ ///
+ ///**Note:** The intended feature set for NavMeshQuery is not yet fully complete.
[Obsolete("The experimental NavMeshQuery struct has been deprecated. Use NavWorld instead.")]
[NativeContainer]
[StructLayout(LayoutKind.Sequential)]
@@ -137,7 +220,7 @@ public void AddDependency(JobHandle job)
[NativeHeader("Modules/AI/Public/NavMeshBindingTypes.h")]
[NativeHeader("Runtime/Math/Matrix4x4.h")]
[StaticAccessor("NavMeshQueryBindingsExperimental", StaticAccessorType.DoubleColon)]
- [NativeType(CodegenOptions.Auto, "NavMeshQueryExp")]
+ [NativeType(CodegenOptions.Auto, "NavMeshQueryExp")]
public struct NavMeshQuery : IDisposable
{
[NativeDisableUnsafePtrRestriction]
@@ -152,6 +235,11 @@ public struct NavMeshQuery : IDisposable
// Keep in sync with kMaxNavMeshNodePoolSize = USHRT_MAX from NavMeshNode.h
const int k_MaxNavMeshNodePoolSize = ushort.MaxValue;
+ ///Creates the object and allocates memory to store NavMesh node information, if required.
+ ///You must specify a pathNodePoolSize greater than 0 to use the NavMeshQuery object for pathfinding methods (BeginFindPath , UpdateFindPath , EndFindPath , GetPathResult ). If the node pool size for the NavMeshQuery object is too small, the pathfinding method returns a status. The range of pathNodePoolSize is 0 through 65,535.
+ /// NavMeshWorld object used as an entry point to the collection of NavMesh objects. This object that can be used by query operations.
+ /// Label indicating the desired life time of the object. (**Known issue:** Currently allocator has no effect).
+ /// The number of nodes temporarily stored in the query during search operations. The maximum number of nodes is 65,535. By default, if unspecified, the number of nodes is set to 0.
public NavMeshQuery(NavMeshWorld world, Allocator allocator, int pathNodePoolSize = 0)
{
if (!world.IsValid())
@@ -168,6 +256,7 @@ public NavMeshQuery(NavMeshWorld world, Allocator allocator, int pathNodePoolSiz
AddQuerySafetyExp(m_NavMeshQuery, m_Safety);
}
+ ///Destroys the NavMeshQuery and deallocates all memory used by it.
public void Dispose()
{
@@ -196,6 +285,23 @@ public void Dispose()
[NativeMethod(IsThreadSafe = true)]
static extern bool HasNodePoolExp(IntPtr navMeshQuery);
+ ///Initiates a pathfinding operation between two locations on the NavMesh.
+ ///The path always begins at the specified location. If the desired end location is not directly accessible, the search algorithm tries to find a valid location nearby.
+ ///
+ ///Calling this method overrides the progress made by this in the previous pathfinding operation.
+ ///
+ /// should be called after this method to process the path search.
+ /// Array of custom cost values for all of the 32 possible area types. Each value must be at least 1.0f . This parameter is optional and defaults to the area costs configured in the project settings.
+ /// Bitmask with values of 1 set at the indices for areas that can be traversed, and values of 0 for areas that are not traversable. This parameter is optional and defaults to , if omitted.
+ /// The start location on the NavMesh for the path.
+ /// The location on the NavMesh where the path ends.
+ ///
+ /// InProgress if the operation was successful and the query is ready to search for a path.
+ ///
+ ///Failure if the query's NavMeshWorld or any of the received parameters are no longer valid.
+ ///
+ ///
+ ///Areas and Costs
public unsafe PathQueryStatus BeginFindPath(NavMeshLocation start, NavMeshLocation end,
int areaMask = NavMesh.AllAreas, NativeArray costs = new NativeArray())
{
@@ -237,6 +343,21 @@ public unsafe PathQueryStatus BeginFindPath(NavMeshLocation start, NavMeshLocati
return BeginFindPathExp(m_NavMeshQuery, start, end, areaMask, costsPtr);
}
+ ///Continues a path search that is in progress.
+ ///The operation needs to have been initialized previously with and it will run until the entire route is found or the specified number of iterations have been executed.
+ ///
+ ///As long as the previous call returned a state of InProgress this method can be called repeatedly, across different frames, until the operation is successful. Use afterwards to prepare the path data for retrieval, along with the number of contained nodes.
+ /// Maximum number of nodes to be traversed by the search algorithm during this call.
+ /// Outputs the actual number of nodes that have been traversed during this call.
+ ///
+ /// InProgress if the search needs to continue further by calling UpdateFindPath again.
+ ///
+ ///Success if the search is completed and a path has been found or not.
+ ///
+ ///Failure if the search for the desired position could not be completed because the NavMesh has changed significantly since the search was initiated.
+ ///
+ ///Additionally the returned value can contain the OutOfNodes flag when the pathNodePoolSize parameter for the NavMeshQuery initialization was not large enough to accommodate the search space.
+ ///
public PathQueryStatus UpdateFindPath(int iterations, out int iterationsPerformed)
{
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
@@ -246,6 +367,18 @@ public PathQueryStatus UpdateFindPath(int iterations, out int iterationsPerforme
return UpdateFindPathExp(m_NavMeshQuery, iterations, out iterationsPerformed);
}
+ ///Obtains the number of nodes in the path that has been computed during a successful operation.
+ ///This method prepares the path data so that can be used afterward to retrieve the actual array of values that make up the path.
+ ///
+ ///**Important:** This method should only be called once at the end of the pathfinding operation. Calling it multiple times may ruin the stored path.
+ /// A reference to an int which will be set to the number of NavMesh nodes in the found path.
+ ///
+ /// Success when the number of nodes in the path was retrieved correctly.
+ ///
+ ///PartialPath when a path was found but it falls short of the desired end location.
+ ///
+ ///Failure when the path size can not be evaluated because the preceding call to UpdateFindPath was not successful.
+ ///
public PathQueryStatus EndFindPath(out int pathSize)
{
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
@@ -255,6 +388,16 @@ public PathQueryStatus EndFindPath(out int pathSize)
return EndFindPathExp(m_NavMeshQuery, out pathSize);
}
+ ///Copies into the provided array the list of NavMesh nodes that form the path found by the NavMeshQuery operation.
+ ///Must be called at the end of a successful - - sequence in order to obtain the resulting path.
+ ///
+ ///Can be called multiple times as long as has not been called for that same query.
+ ///
+ ///If the resulting path, stored in the query, is longer than the length of the provided array, the nodes are still copied (from the beginning of the path up to the array's length).
+ ///
+ ///**Important:** If the start NavMesh node of the path has been removed by a NavMesh modification since the initial BeginFindPath call of the pathfinding operation, the returned path will be empty.
+ /// Data array to be filled with the sequence of NavMesh nodes that comprises the found path.
+ ///Number of path nodes successfully copied into the provided array.
public unsafe int GetPathResult(NativeSlice path)
{
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
@@ -285,12 +428,17 @@ public unsafe int GetPathResult(NativeSlice path)
[NativeMethod(IsThreadSafe = true)]
static extern bool IsValidPolygonExp(IntPtr navMeshQuery, PolygonId polygon);
+ ///Returns true if the node referenced by the specified is active in the NavMesh.
+ ///You can make NavMesh nodes invalid when you remove the NavMesh surface or the links they belong to, or when you modify the NavMesh in their region, replacing them. You can remove the NavMesh surface and links with calls to , . To modify the NavMesh, call or use a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/NavMeshObstacle.html">NavMeshObstacle</a> to carve it.
+ /// Identifier of the NavMesh node to be checked.
public bool IsValid(PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
return polygon.polyRef != 0 && IsValidPolygonExp(m_NavMeshQuery, polygon);
}
+ ///Returns true if the node referenced by the contained in the is active in the NavMesh.
+ /// Location on the NavMesh to be checked. Same as checking location.polygon directly.
public bool IsValid(NavMeshLocation location)
{
return IsValid(location.polygon);
@@ -298,6 +446,10 @@ public bool IsValid(NavMeshLocation location)
[NativeMethod(IsThreadSafe = true)]
static extern int GetAgentTypeIdForPolygonExp(IntPtr navMeshQuery, PolygonId polygon);
+ ///Returns the identifier of the agent type the NavMesh was baked for or for which the link has been configured.
+ ///When NavMesh surfaces are baked or links are configured the **Agent Type** allowed to use them needs to be specified. Each **Agent Type** is identified by a unique integer. Operations such as , , , , and all require an agent type to be specified to distinguish between NavMeshes built for different agent configurations.
+ /// Identifier of a node from a NavMesh surface or link.
+ ///Agent type identifier.
public int GetAgentTypeIdForPolygon(PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -310,6 +462,13 @@ public int GetAgentTypeIdForPolygon(PolygonId polygon)
[NativeMethod(IsThreadSafe = true)]
static extern PathQueryStatus GetClosestPointOnPolyExp(IntPtr navMeshQuery, PolygonId polygon, Vector3 position, out Vector3 nearest);
+ ///Returns a valid for a position and a polygon provided by the user.
+ ///The returned position will be the point on the surface of the required NavMesh polygon that is closest to the specified position.
+ ///
+ ///Other methods for obtaining reliable positions on the NavMesh are: , and .
+ /// World position of the to be created.
+ /// Valid identifier for the NavMesh node.
+ ///Object containing the desired position and NavMesh node.
public NavMeshLocation CreateLocation(Vector3 position, PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -320,6 +479,20 @@ public NavMeshLocation CreateLocation(Vector3 position, PolygonId polygon)
[NativeMethod(IsThreadSafe = true)]
static extern NavMeshLocation MapLocationExp(IntPtr navMeshQuery, Vector3 position, Vector3 extents, int agentTypeID, int areaMask = NavMesh.AllAreas);
+ ///Finds the closest point and on the NavMesh for a given world position.
+ ///The search only applies to the specified type of NavMesh surface, for one or more desired area types and is limited to within the specified search area. It does not search for positions on <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLinks</a> or OffMeshLinks .
+ ///
+ ///Nearby NavMesh surfaces directly above or below the specified position are preferred. When there are none up or down within the specified search extents the surfaces closest sideways are sampled.
+ /// World position for which the closest point on the NavMesh needs to be found.
+ /// Maximum distance, from the specified position , expanding along all three axes, within which NavMesh surfaces are searched.
+ /// Identifier for the agent type whose NavMesh surfaces should be selected for this operation. The Humanoid agent type exists for all NavMeshes and has an ID of 0. Other agent types can be defined manually through the Editor. A separate NavMesh surface needs to be baked for each agent type.
+ /// Bitmask used to represent areas of the NavMesh that should (value of 1) or shouldn't (values of 0) be sampled. This parameter is optional and defaults to if unspecified.
+ ///An object with position and valid - when a point on the NavMesh has been found.
+ ///
+ ///An invalid object - when no NavMesh surface with the desired features has been found within the search area.
+ ///
+ ///
+ ///Areas and Costs
public NavMeshLocation MapLocation(Vector3 position, Vector3 extents, int agentTypeID, int areaMask = NavMesh.AllAreas)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -328,6 +501,15 @@ public NavMeshLocation MapLocation(Vector3 position, Vector3 extents, int agentT
[NativeMethod(IsThreadSafe = true)]
static extern unsafe void MoveLocationsExp(IntPtr navMeshQuery, void* locations, void* targets, void* areaMasks, int count);
+ ///Translates a series of NavMesh locations to other positions without losing contact with the surface.
+ ///Does the exact same thing as only it acts sequentially on a batch of locations, given their respective targets and area filters. All three array parameters must have the same length.
+ ///
+ ///The results are returned in-place in the locations array.
+ /// Array of positions to be moved across the NavMesh surface. At the end of the method call this array contains the resulting locations.
+ /// World positions to be used as movement targets by the agent.
+ /// Filters for the areas which can be traversed during the movement to each of the locations.
+ ///
+ ///
public unsafe void MoveLocations(NativeSlice locations, NativeSlice targets, NativeSlice areaMasks)
{
if (locations.Length != targets.Length || locations.Length != areaMasks.Length)
@@ -339,6 +521,13 @@ public unsafe void MoveLocations(NativeSlice locations, NativeS
[NativeMethod(IsThreadSafe = true)]
static extern unsafe void MoveLocationsInSameAreasExp(IntPtr navMeshQuery, void* locations, void* targets, int count, int areaMask);
+ ///Translates a series of NavMesh locations to other positions without losing contact with the surface, given one common area filter for all of them.
+ ///Does the exact same thing as only it applies the same area filter to all the movements.
+ /// Array of positions to be moved across the NavMesh surface. At the end of the method call this array contains the resulting locations.
+ /// World positions you want the agent to reach when moving to each of the locations.
+ /// Filters for the areas which can be traversed during the movement to each of the locations.
+ ///
+ ///
public unsafe void MoveLocationsInSameAreas(NativeSlice locations, NativeSlice targets, int areaMask = NavMesh.AllAreas)
{
if (locations.Length != targets.Length)
@@ -350,6 +539,21 @@ public unsafe void MoveLocationsInSameAreas(NativeSlice locatio
[NativeMethod(IsThreadSafe = true)]
static extern NavMeshLocation MoveLocationExp(IntPtr navMeshQuery, NavMeshLocation location, Vector3 target, int areaMask);
+ ///Translates a NavMesh location to another position without losing contact with the surface.
+ ///Returns the location on the NavMesh that is closest to the target position and that also has a continuous connection on the NavMesh surface through the allowed area types all the way to the start position specified by the location parameter. If the target position is outside the edges of the surface or of its allowed areas, a position at the edge is returned.
+ ///
+ ///The movement does not cross <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLinks</a> or OffMeshLinks .
+ ///
+ ///The result might not be accurate (the closest) if the pathNodePoolSize value in the NavMeshQuery initialization was not large enough to accommodate all the nodes that needed to be traversed in order to find a connection between location.position and target .
+ /// Position to be moved across the NavMesh surface.
+ /// World position you require the agent to move to.
+ /// Bitmask with values of 1 set at the indices corresponding to areas that can be traversed, and with values of 0 for areas that should not be traversed. This parameter can be omitted, in which case it defaults to .
+ ///A new location on the NavMesh placed as closely as possible to the specified target position.
+ ///
+ ///The start location is returned when that start is inside an area which is not allowed by the areaMask .
+ ///
+ ///
+ ///Areas and Costs
public NavMeshLocation MoveLocation(NavMeshLocation location, Vector3 target, int areaMask = NavMesh.AllAreas)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -358,6 +562,20 @@ public NavMeshLocation MoveLocation(NavMeshLocation location, Vector3 target, in
[NativeMethod(IsThreadSafe = true)]
static extern bool GetPortalPointsExp(IntPtr navMeshQuery, PolygonId polygon, PolygonId neighbourPolygon, out Vector3 left, out Vector3 right);
+ ///Obtains the end points of the line segment common to two adjacent NavMesh nodes.
+ ///For two polygons that are part of a NavMesh surface, this method returns the edge where both polygons meet. If the two polygons are in different NavMesh tiles the connected edges can be of different length or have different start and end positions from each other. If this happens the resulting separation edge is the overlapping part of the edges, which may be shorter than either of the individual edges.
+ ///
+ ///When one node is a link and the other is a polygon, the returned points are placed where the link intersects the polygon.
+ ///
+ ///The resulting positions are expressed in world space and can be transformed into a NavMesh's local space by the use of .
+ /// First NavMesh node.
+ /// Second NavMesh node.
+ /// One of the world points for the resulting separation edge which must be passed through when traversing between the two specified nodes. This point is the left side of the edge when traversing from the first node to the second.
+ /// One of the world points for the resulting separation edge which must be passed through when traversing between the two specified nodes. This point is the right side of the edge when traversing from the first node to the second.
+ ///
+ /// True if a connection exists between the two NavMesh nodes.
+ ///False if no connection exists between the two NavMesh nodes.
+ ///
public bool GetPortalPoints(PolygonId polygon, PolygonId neighbourPolygon, out Vector3 left, out Vector3 right)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -366,6 +584,21 @@ public bool GetPortalPoints(PolygonId polygon, PolygonId neighbourPolygon, out V
[NativeMethod(IsThreadSafe = true)]
static extern Matrix4x4 PolygonLocalToWorldMatrixExp(IntPtr navMeshQuery, PolygonId polygon);
+ ///Returns the transformation matrix of the NavMesh surface that contains the specified NavMesh node.
+ ///
+ /// surfaces have their transforms defined by the position and rotation values declared at the moment when they were baked with , or as part of a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/NavMeshSurface.html">NavMeshSurface</a>, or by explicitly setting the values for and .
+ ///
+ ///Custom transforms for s can further be specified when they are created with explicit position and rotation values passed to the (data, position, rotation) method.
+ ///
+ ///**Important:** This method does not return the position and orientation of a single NavMesh polygon. It returns the position of the surface that owns the polygon.
+ ///
+ ///**Known issue:** Identity matrix is returned instead of the actual transform for NavMeshLinks that have been instantiated with a call to (link, position, rotation).
+ /// NavMesh node for which its owner's transform must be determined.
+ ///Transformation matrix for the surface owning the specified polygon.
+ ///
+ /// when the NavMesh node is a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> or an .
+ ///
+ ///
public Matrix4x4 PolygonLocalToWorldMatrix(PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -374,6 +607,17 @@ public Matrix4x4 PolygonLocalToWorldMatrix(PolygonId polygon)
[NativeMethod(IsThreadSafe = true)]
static extern Matrix4x4 PolygonWorldToLocalMatrixExp(IntPtr navMeshQuery, PolygonId polygon);
+ ///Returns the inverse transformation matrix of the NavMesh surface that contains the specified NavMesh node.
+ ///In contrast to the returned matrix can be used for transforming a world-coordinates position into the local coordinate system of the NavMesh surface owning the specified polygon.
+ ///
+ ///**Important:** This method does not return the inverse position and orientation of a single NavMesh polygon. It returns the inverse position and orientation of the surface that owns the polygon.
+ ///
+ ///**Known issue:** Identity matrix is returned instead of the actual inverse transform for NavMeshLinks that have been instantiated with a call to (linkData, position, rotation).
+ /// NavMesh node for which its owner's inverse transform must be determined.
+ ///Inverse transformation matrix of the surface owning the specified polygon.
+ ///
+ /// when the NavMesh node is a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> or an .
+ ///
public Matrix4x4 PolygonWorldToLocalMatrix(PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -382,6 +626,18 @@ public Matrix4x4 PolygonWorldToLocalMatrix(PolygonId polygon)
[NativeMethod(IsThreadSafe = true)]
static extern NavMeshPolyTypes GetPolygonTypeExp(IntPtr navMeshQuery, PolygonId polygon);
+ ///Returns whether the NavMesh node is a polygon or a link.
+ ///The type can be determined even after the specified node has become invalid in the query's NavMeshWorld.
+ ///
+ ///**Known issue, to be fixed:** If the query's is invalid for any reason, the method returns .
+ /// Identifier of a node from a NavMesh surface or link.
+ ///
+ /// Ground when the node is a polygon on a NavMesh surface.
+ ///
+ ///OffMeshConnection when the node is a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> or an .
+ ///
+ ///
+ ///
public NavMeshPolyTypes GetPolygonType(PolygonId polygon)
{
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
@@ -396,6 +652,84 @@ public NavMeshPolyTypes GetPolygonType(PolygonId polygon)
static extern unsafe PathQueryStatus RaycastExp(IntPtr navMeshQuery, NavMeshLocation start, Vector3 targetPosition,
int areaMask, void* costs, out NavMeshHit hit, void* path, out int pathCount, int maxPath);
+ ///Trace a line between two points on the NavMesh.
+ ///This method is similar to , both of them sharing the same underlying implementation.
+ ///
+ ///The properties that make this one different are:
+ ///
+ ///- it can be used in parallel [jobs](xref:JobSystem);
+ ///
+ ///- it returns status flags indicating whether the operation succeeded or failed;
+ ///
+ ///- the reported hit.distance is affected by the area costs;
+ ///
+ ///- the resulting hit.position is not adjusted on the vertical axis according to the <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/HeightMesh.html">HeightMesh</a>, if that exists;
+ ///
+ ///- it has the variant described below that returns also the list of polygons through which the ray passes.
+ ///
+ ///
+ ///
+ ///The returned hit.distance represents the straight line between the start and termination point. It also takes into account the list of the provided area costs. It is the result of summing up all the distances covered by the ray over each separate area, multiplied by the cost of that respective area.
+ ///
+ ///
+ ///
+ ///First, the start location is verified to be valid in the NavMeshWorld, and the target point is mapped on the NavMesh. Then, a ray is traced from the start point towards the target. If the computation is successful, the hit data is filled with information about the furthest point that the ray has reached. This happens regardless of whether the path from the source to target has been obstructed.
+ ///
+ ///If the computation fails, the returned hit is filled with invalid data. Most notably, the hit.distance field gets the value positiveInfinity .
+ ///
+ ///If the raycast terminates on an outer edge, hit.mask is 0; otherwise it contains the area mask of the blocking polygon.
+ ///
+ ///You can use this function to check if an agent can walk unobstructed between two points on the NavMesh. For example, if your character has an evasive dodge move that needs space, you can shoot a ray from the character's location to multiple directions. This finds a spot where the character can dodge to.
+ ///
+ ///The is different from the Physics raycast. The NavMeshQuery.Raycast can detect all kinds of navigation obstructions, for example holes in the ground. It can also climb up slopes, if the area is navigable.
+ /// Holds the properties of the raycast resulting location.
+ /// The start location of the ray on the NavMesh. start.polygon must be of the type .
+ /// The desired end of the ray, in world coordinates.
+ /// Bitmask that correlates index positions with area types. The index goes from 0 to 31. In each relevant index position, you have to set the value to either 1 or 0. 1 indicates area types that the ray can pass through. 0 indicates area types that block the ray. This parameter is optional. If you leave out this parameter, it defaults to . To learn more, see: <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/AreasAndCosts.html">Areas and Costs</a>.
+ /// Array of custom cost values for all of the 32 possible area types. They act as multipliers to the distance reported by the ray when crossing various areas. This parameter is optional. If you omit it, it defaults to the area costs that you configured in the Project settings. To learn more, see .
+ ///
+ /// Success if the ray can be correctly traced using the provided arguments.
+ ///
+ ///Failure if the start location is not valid in the query's NavMeshWorld, or if it is inside an area not permitted by the areaMask argument, or when it is on a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a>/ .
+ ///
+ /// ());
+ /// if ((status & PathQueryStatus.Success) != 0)
+ /// {
+ /// Debug.DrawLine(transform.position, target.position, m_Hit.hit ? Color.red : Color.green);
+ ///
+ /// if (m_Hit.hit)
+ /// Debug.DrawRay(m_Hit.position, Vector3.up, Color.red);
+ /// }
+ /// }
+ ///
+ /// void OnDisable()
+ /// {
+ /// m_NavQuery.Dispose();
+ /// }
+ ///}
+ ///]]>
+ ///
public unsafe PathQueryStatus Raycast(out NavMeshHit hit, NavMeshLocation start, Vector3 targetPosition,
int areaMask = NavMesh.AllAreas, NativeArray costs = new NativeArray())
{
@@ -415,6 +749,66 @@ public unsafe PathQueryStatus Raycast(out NavMeshHit hit, NavMeshLocation start,
return status;
}
+ ///Trace a line between two points on the NavMesh, and return the list of polygons through which it passed.
+ ///Even if the path buffer is too small it will still hold as many polygons as it has room for, starting from the ray's origin location.
+ /// Holds the properties of the raycast resulting location.
+ /// A buffer that will be filled with the sequence of polygons through which the ray passes.
+ /// The reported number of polygons through which the ray has passed, all stored in the path buffer. It will not be greater than path.Length .
+ /// The start location of the ray on the NavMesh. start.polygon must be of the type .
+ /// The desired end of the ray, in world coordinates.
+ /// A bitfield that specifies which NavMesh areas can be traversed when the ray is traced. This parameter is optional. If you do not fill out this parameter, it defaults to .
+ /// Cost multipliers that affect the distance reported by the ray over different area types. This parameter is optional. If you omit it, it defaults to the area costs that you configured in the Project settings.
+ ///
+ /// Success if the ray can be correctly traced using the provided arguments.
+ ///
+ ///Failure if the start location is not valid in the query's NavMeshWorld, or if it is inside an area not permitted by the areaMask argument, or when it is on a <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a>/ .
+ ///
+ ///BufferTooSmall is part of the returned flags when the provided path buffer is not large enough to hold all the polygons that the ray passed through.
+ ///
+ /// m_Path;
+ /// int m_PathCount;
+ ///
+ /// void OnEnable()
+ /// {
+ /// m_Path = new NativeArray(3, Allocator.Persistent);
+ /// m_NavQuery = new NavMeshQuery(NavMeshWorld.GetDefaultWorld(), Allocator.Persistent);
+ /// }
+ ///
+ /// void Update()
+ /// {
+ /// var startLocation = m_NavQuery.MapLocation(transform.position, Vector3.one, 0);
+ /// PathQueryStatus status = m_NavQuery.Raycast(out m_Hit, m_Path, out m_PathCount, startLocation, target.position, NavMesh.AllAreas, new NativeArray());
+ /// if ((status & PathQueryStatus.Success) != 0)
+ /// {
+ /// var bufferTooSmall = (status & PathQueryStatus.BufferTooSmall) != 0;
+ /// Debug.DrawLine(transform.position, m_Hit.position, bufferTooSmall ? Color.black : Color.green);
+ ///
+ /// if (m_Hit.hit)
+ /// Debug.DrawRay(m_Hit.position, Vector3.up, Color.red);
+ /// }
+ /// }
+ ///
+ /// void OnDisable()
+ /// {
+ /// m_NavQuery.Dispose();
+ /// m_Path.Dispose();
+ /// }
+ ///}
+ ///]]>
+ ///
+ ///
public unsafe PathQueryStatus Raycast(out NavMeshHit hit, NativeSlice path, out int pathCount,
NavMeshLocation start, Vector3 targetPosition,
int areaMask = NavMesh.AllAreas, NativeArray costs = new NativeArray())
@@ -440,6 +834,116 @@ static extern unsafe PathQueryStatus GetEdgesAndNeighborsExp(IntPtr navMeshQuery
void* verts, void* neighbors, void* edgeIndices,
out int vertCount, out int neighborsCount);
+ ///Retrieves the vertices of a given node and the identifiers of all the navigation nodes to which it connects.
+ ///A polygon of a NavMesh surface connects to all other neighboring polygons with which it shares an edge as well as all the OffMeshLinks or NavMeshLinks that leave from anywhere on its surface. The polygon does not connect to other polygons with which it shares only a vertex.
+ ///
+ ///Each point returned in the edgeVertices array represents the start of a node 's edge and the subsequent element in the array is the end point of that edge. All vertices form a closed polygonal line. The last and first elements define the last edge.
+ ///
+ ///
+ ///
+ ///An off-mesh link connects to all the NavMesh polygons that each end of the link intersects with, regardless of whether the link is unidirectional.
+ ///
+ ///For link nodes the returned edgeVertices array contains two pairs of points at indices [0]-[1] and [2]-[3] that define the end points of the start and end edges of the link, in this order. These are the world positions established at the moment when the link is instantiated in the NavMesh world. For nodes added through <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/NavMeshLink.html">NavMesh Link</a> or <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/OffMeshLink.html">OffMesh Link</a> components the pairs contain the same value in both of their elements.
+ ///
+ ///
+ ///
+ ///A node from the neighbors array lies at the edge returned in edgeIndices at the same index.
+ ///
+ ///If both the given node and its neighbor are NavMesh polygons , then the corresponding edgeIndices value represents the index of the polygon edge that leads from node to the neighbor. E.g. edgeVertices[edgeIndices[2]] represents the start point of the edge that is common between node and the neighbors[2] node, and edgeVertices[edgeIndices[2] + 1] is the end point of that edge.
+ ///
+ ///A NavMesh polygon can have a maximum of 6 edges. This means the edgeIndices value corresponding to a polygon-polygon connection is between 0 and 5, inclusive. An edge usually connects only the two polygons that share it, but edges that sit at a tile border can connect one polygon in the first tile to multiple polygons in the second tile. In this case, edgeIndices report the same value for all of those neighbors.
+ ///
+ ///If either the given node or the neighbor is a link , then the corresponding edgeIndices value represents the side on the link where the connection is made: 0 for start and 2 for end . When the node is a polygon and the neighbor is a link the value acts only as information about the side of the link where the two nodes connect and should not be used as an index in the edgeVertices array.
+ ///
+ ///When the neighbors and edgeIndices buffers both have positive capacity, they must be the same size, otherwise you will encounter an ArgumentException when this method executes in the Editor.
+ ///
+ ///
+ ///
+ ///You can set any of the buffers to have zero capacity for the cases when you do not need the results.
+ ///
+ ///
+ ///
+ ///The returned verticesCount and neighborsCount values express the number of elements that comprise the result in the output buffers of sufficient size. Buffers that are not large enough are still filled with valid nodes up to their full capacity.
+ ///
+ ///
+ ///
+ ///The five result parameters (edgeVertices , neighbors , edgeIndices , verticesCount and neighborsCount ) do not act as input and do not change the internal navigation data in any way. Unity only modifies them in the case when the operation returns a Success status.
+ /// Identifier of a node from a NavMesh surface, <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> or for which the vertices and neighbors need to be retrieved.
+ /// The result buffer that contains the world positions describing the geometry of the input navigation node . It can have zero capacity.
+ ///
+ ///Polygonal nodes of the NavMesh have a minimum of 3 and a maximum of 6 vertices.
+ ///
+ ///OffMeshConnection nodes are always represented by 4 vertices, regardless of their width.
+ /// The result buffer that holds the identifiers of all the navigation nodes immediately reachable from the given node . It can have zero capacity.
+ /// The helper result buffer that maps each neighbor node to an edge of the given node . It can have zero capacity.
+ ///
+ ///The index of an element in edgeIndices is also an index in the neighbors array and the value of that edgeIndices element is an index in the edgeVertices array.
+ /// The total number of vertices that describe the geometry of the input node . This is independent of the capacity of the vertices result buffer.
+ /// The total number of navigation nodes the input node connects to. This is independent of the capacity of the result buffers (neighbors and edgeIndices ).
+ ///
+ /// Success if Unity can evaluate the neighbors and vertices of the specified node, regardless of the result. The verticesCount and neighborsCount are always valid in this case.
+ ///
+ ///Failure if Unity can not use the node identifier to retrieve the neighbors or geometry information. Unity does not modify any of the five result parameters (edgeVertices , neighbors , edgeIndices , verticesCount or neighborsCount ) in this case.
+ ///
+ ///InvalidParam is part of the returned flags if the specified navigation node is not valid in the query's NavMeshWorld.
+ ///
+ ///BufferTooSmall is part of the PathQueryStatus flags, that Unity returns from this function, when any of the result buffers you provide are not large enough to hold all the neighbor nodes the input node connects to or all of its edge vertices.
+ ///
+ /// (6, Allocator.Temp);
+ /// var neighbors = new NativeArray(10, Allocator.Temp);
+ /// var edgeIndices = new NativeArray(neighbors.Length, Allocator.Temp);
+ /// int totalVertices;
+ /// int totalNeighbors;
+ ///
+ /// var location = query.MapLocation(transform.position, Vector3.one, 0);
+ ///
+ /// var queryStatus = query.GetEdgesAndNeighbors(
+ /// location.polygon, vertices, neighbors, edgeIndices,
+ /// out totalVertices, out totalNeighbors);
+ ///
+ /// var color = (queryStatus & PathQueryStatus.Success) != 0 ? Color.green : Color.red;
+ /// Debug.DrawLine(transform.position - Vector3.up, transform.position + Vector3.up, color);
+ ///
+ /// for (int i = 0, j = totalVertices - 1; i < totalVertices; j = i++)
+ /// {
+ /// Debug.DrawLine(vertices[i], vertices[j], Color.grey);
+ /// }
+ ///
+ /// for (var i = 0; i < totalNeighbors; i++)
+ /// {
+ /// if (query.GetPolygonType(neighbors[i]) == NavMeshPolyTypes.OffMeshConnection)
+ /// {
+ /// // The link neighbor is not connected through any of the polygon's edges.
+ /// // Call GetEdgesAndNeighbors() on this specific neighbor in order to retrieve its edges.
+ /// continue;
+ /// }
+ ///
+ /// var start = edgeIndices[i];
+ /// var end = (start + 1) % totalVertices;
+ /// var neighborColor = Color.Lerp(Color.yellow, Color.magenta, 1f * start / (totalVertices - 1));
+ /// Debug.DrawLine(vertices[start], vertices[end], neighborColor);
+ /// }
+ ///
+ /// query.Dispose();
+ /// vertices.Dispose();
+ /// neighbors.Dispose();
+ /// edgeIndices.Dispose();
+ /// }
+ ///}
+ ///]]>
+ ///
+ ///
+ ///
public unsafe PathQueryStatus GetEdgesAndNeighbors(PolygonId node,
NativeSlice edgeVertices, NativeSlice neighbors, NativeSlice edgeIndices,
out int verticesCount, out int neighborsCount)
diff --git a/Modules/AI/NavMeshPath.bindings.cs b/Modules/AI/NavMeshPath.bindings.cs
index ebd79f7705..008158ae84 100644
--- a/Modules/AI/NavMeshPath.bindings.cs
+++ b/Modules/AI/NavMeshPath.bindings.cs
@@ -10,16 +10,22 @@
namespace UnityEngine.AI
{
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
- // Status of path.
+ ///Status of path.
[MovedFrom("UnityEngine")]
public enum NavMeshPathStatus
{
- PathComplete = 0, // The path terminates at the destination.
- PathPartial = 1, // The path cannot reach the destination.
- PathInvalid = 2 // The path is invalid.
+ ///The path terminates at the destination.
+ PathComplete = 0,
+ ///The path cannot reach the destination.
+ PathPartial = 1,
+ ///The path is not valid.
+ ///Refer to the documentation of the returning method or property for more information. , .
+ ///
+ PathInvalid = 2
}
- // Path navigation.
+ ///A path as calculated by the navigation system.
+ ///The path is represented as a list of waypoints stored in the array. These points are not set directly from user scripts but a NavMeshPath with points correctly assigned is returned by the function and the property.
[NativeHeader("Modules/AI/NavMeshPath.bindings.h")]
[StructLayout(LayoutKind.Sequential)]
[MovedFrom("UnityEngine")]
@@ -28,6 +34,7 @@ public sealed class NavMeshPath
internal IntPtr m_Ptr;
internal Vector3[] m_Corners;
+ ///NavMeshPath constructor.
public NavMeshPath()
{
m_Ptr = InitializeNavMeshPath();
@@ -45,6 +52,12 @@ public NavMeshPath()
[FreeFunction("NavMeshPathScriptBindings::DestroyNavMeshPath", IsThreadSafe = true)]
static extern void DestroyNavMeshPath(IntPtr ptr);
+ ///Calculate the corners for the path.
+ ///This function is similar to the property except that the results are returned in the supplied array.
+ ///
+ ///Note that this function expects the supplied array to have at least 2 elements.
+ /// Array to store path corners.
+ ///The number of corners along the path - including start and end points.
[FreeFunction("NavMeshPathScriptBindings::GetCornersNonAlloc", HasExplicitThis = true)]
public extern int GetCornersNonAlloc([Out] Vector3[] results);
@@ -54,7 +67,7 @@ public NavMeshPath()
[FreeFunction("NavMeshPathScriptBindings::ClearCornersInternal", HasExplicitThis = true)]
extern void ClearCornersInternal();
- // Erase all corner points from path.
+ ///Erase all corner points from path.
public void ClearCorners()
{
ClearCornersInternal();
@@ -67,10 +80,32 @@ void CalculateCorners()
m_Corners = CalculateCornersInternal();
}
- // Corner points of path. (RO)
+ ///Corner points of the path.
+ ///Also known as "waypoints", the corners define the places along a path where it changes direction (ie, the path consists of a number of straight-line moves between corners).
+ ///
+ ///
+ ///
public Vector3[] corners { get { CalculateCorners(); return m_Corners; } }
- // Status of the path. (RO)
+ ///Status of the path.
+ ///This reports whether the path reaches the target, reaches part of the way to the target, or is just not valid. Among other reasons, a path returns if it can't determine the nearest polygon of the source or target position, or if the path would have been a partial result, but the point closest to the target on the final polygon could not be determined. These situations are rare, and may arise if the navigation mesh is being changed while a path is being calculated.
public extern NavMeshPathStatus status { get; }
internal static class BindingsMarshaller
diff --git a/Modules/AI/Public/NavMeshBindingTypes.bindings.cs b/Modules/AI/Public/NavMeshBindingTypes.bindings.cs
index 5e5cde5d0e..b187070e40 100644
--- a/Modules/AI/Public/NavMeshBindingTypes.bindings.cs
+++ b/Modules/AI/Public/NavMeshBindingTypes.bindings.cs
@@ -9,49 +9,141 @@
namespace UnityEngine.AI
{
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Bitmask used for operating with debug data from the NavMesh build process.
+ ///Used in two situations:
+ ///
+ ///- within to specify which debug data to retain after the build process has completed, preserving the world position and orientation;
+ ///
+ ///- as a parameter of to control which of the available debug data types to display for a specified NavMesh.
+ ///
[Flags]
public enum NavMeshBuildDebugFlags
{
+ ///No debug data from the NavMesh build process is taken into consideration.
+ ///
+ ///
None = 0,
+ ///The triangles of all the geometry that is used as a base for computing the new NavMesh.
+ ///
+ ///
+ ///
+ ///
InputGeometry = 1 << 0,
+ ///The voxels produced by rasterizing the source geometry into walkable and unwalkable areas.
+ ///
+ ///
+ ///
Voxels = 1 << 1,
+ ///The segmentation of the traversable surfaces into smaller areas necessary for producing simple polygons.
+ ///
+ ///
Regions = 1 << 2,
+ ///The contours that follow precisely the edges of each surface region.
+ ///
+ ///
RawContours = 1 << 3,
+ ///Contours bounding each of the surface regions, described through fewer vertices and straighter edges compared to .
+ ///
+ ///
SimplifiedContours = 1 << 4,
+ ///Meshes of convex polygons constructed within the unified contours of adjacent regions.
+ ///
+ ///
PolygonMeshes = 1 << 5,
+ ///The triangulated meshes with height details that better approximate the source geometry.
+ ///
+ ///
PolygonMeshesDetail = 1 << 6,
+ ///All debug data from the NavMesh build process is taken into consideration.
+ ///
+ ///
All = unchecked((int)(~(~0U << 7)))
}
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Used with to define the shape for building NavMesh.
public enum NavMeshBuildSourceShape
{
+ ///Describes a Mesh source for use with . Mesh sources must be positioned within 100,000 units of the world origin and must not exceed 100,000 units in any axis-aligned dimension.
Mesh = 0,
+ ///Describes a source for use with .
Terrain = 1,
+ ///Describes a box primitive for use with .
Box = 2,
+ ///Describes a sphere primitive for use with .
Sphere = 3,
+ ///Describes a capsule primitive for use with .
Capsule = 4,
+ ///Describes a ModifierBox source for use with .
+ ///This shape changes the area type of the walkable surface inside the box. Because this modification happens in a voxel representation of the scene, NavMesh does not follow the outline of the box precisely. If several ModifierBoxes overlap, and have different area types, the area type with the highest index takes precedence. A ModifierBox that you set to Not Walkable takes precedence over any other ModifierBoxes, regardless of their area type. This is useful when you need to block out an area.
+ ///Areas and Costs
ModifierBox = 5
}
// Keep this enum in sync with the one defined in "NavMeshBindingTypes.h"
+ ///Used for specifying the type of geometry to collect. Used with .
public enum NavMeshCollectGeometry
{
+ ///Collect meshes form the rendered geometry.
RenderMeshes = 0,
+ ///Collect geometry from the 3D physics collision representation.
PhysicsColliders = 1
}
- // Struct containing source geometry data and annotation for runtime navmesh building
+ ///The input to the NavMesh builder is a list of NavMesh build sources.
+ ///Their shape can be one of the following: mesh, terrain, box, sphere, or capsule. Each of them is described by a NavMeshBuildSource struct.
+ ///
+ ///You can specify a build source by filling a NavMeshBuildSource struct and adding that to the list of sources that are passed to the bake function. Alternatively, you can use the collect API to quickly create NavMesh build sources from available render meshes or physics colliders. See .
+ ///
+ ///If you use this function at runtime, any meshes with read/write access disabled will not be processed or included in the final NavMesh. See .
+ ///
+ ///
+ ///
[UsedByNativeCode]
[NativeHeader("Modules/AI/Public/NavMeshBindingTypes.h")]
public struct NavMeshBuildSource
{
+ ///Describes the local to world transformation matrix of the build source. That is, position and orientation and scale of the shape.
public Matrix4x4 transform { get { return m_Transform; } set { m_Transform = value; } }
+ ///Describes the dimensions of the shape.
+ ///Used only for the primitive shapes: Sphere, Capsule, Box.
+ ///
+ ///• Sphere: size is the dimensions of a box enclosing the sphere (i.e., x, y, and z are all equal to the diameter).
+ ///
+ ///• Box: size is the dimensions of the box.
+ ///
+ ///• Capsule: size is the dimensions of a box enclosing the capsule (i.e., x and z are equal to the diameter of the capsule and y is the height).
public Vector3 size { get { return m_Size; } set { m_Size = value; } }
+ ///The type of the shape this source describes.
+ ///
public NavMeshBuildSourceShape shape { get { return m_Shape; } set { m_Shape = value; } }
+ ///Describes the area type of the NavMesh surface for this object.
public int area { get { return m_Area; } set { m_Area = value; } }
+ ///Enables the links generation for this object.
+ ///When build sources are obtained using , this value can be affected by using . If this value is true and links parameters are valid in this source will be considered during links generation.
public bool generateLinks { get { return m_GenerateLinks != 0; } set { m_GenerateLinks = value ? 1 : 0; } }
+ ///Describes the object referenced for Mesh and Terrain types of input sources.
+ ///Used only for the types: and .
public Object sourceObject { get { return InternalGetObject(m_EntityId); } set { m_EntityId = value != null ? value.GetEntityId() : EntityId.None; } }
+ ///Points to the owning component - if available, otherwise null.
+ ///When build sources are obtained using , this value typically refers to a mesh or collider component - however for shared meshes it will be null.
+ ///
public Component component { get { return InternalGetComponent(m_ComponentID); } set { m_ComponentID = value != null ? value.GetEntityId() : EntityId.None; } }
Matrix4x4 m_Transform;
@@ -69,17 +161,34 @@ public struct NavMeshBuildSource
static extern Object InternalGetObject(EntityId instanceID);
}
- // Struct containing source geometry data and annotation for runtime navmesh building
+ ///The NavMesh build markup allows you to control how certain objects are treated during the NavMesh build process, specifically when collecting sources for building.
+ ///You can override the area type or specify that certain objects should be excluded from collected sources. The markup can be applied hierarchically or to only the specified object.
+ ///
[NativeHeader("Modules/AI/Public/NavMeshBindingTypes.h")]
public struct NavMeshBuildMarkup
{
+ ///Use this to specify whether the area type of the GameObject and its children should be overridden by the area type specified in this struct.
public bool overrideArea { get { return m_OverrideArea != 0; } set { m_OverrideArea = value ? 1 : 0; } }
+ ///The area type to use when override area is enabled.
public int area { get { return m_Area; } set { m_Area = value; } }
+ ///Set this to true in order to enable the property.
+ ///In the case when a NavMeshBuildMarkup is used to change only the area type of an object, overrideIgnore should be set to false so that the ignoreFromBuild property will not have any effect.
+ ///
+ /// If none of the objects in a hierarchy are marked with ignoreFromBuild set to true then no objects in that hierarchy will be ignored while building the NavMesh.
public bool overrideIgnore { get { return m_InheritIgnoreFromBuild == 0; } set { m_InheritIgnoreFromBuild = value ? 0: 1; } }
+ ///Use this to specify whether the GameObject and its children should be ignored.
+ ///If you set this to true , the GameObject and its children will not be included as part of the NavMesh.
+ ///
+ /// Set to true in order for this property to have the intended effect. When overrideIgnore is false this property inherits the value from the markup of a parent object, if that exists, otherwise it is set to false .
public bool ignoreFromBuild { get { return m_IgnoreFromBuild != 0; } set { m_IgnoreFromBuild = value ? 1 : 0; } }
+ ///Use this to specify whether the default links generation condition for the GameObject and its children should be overridden by the generateLinks option specified in this struct.
public bool overrideGenerateLinks { get { return m_OverrideGenerateLinks != 0; } set { m_OverrideGenerateLinks = value ? 1 : 0; } }
+ ///Use this to specify whether the GameObject and its children should be included in the link generation process.
public bool generateLinks { get { return m_GenerateLinks != 0; } set { m_GenerateLinks = value ? 1 : 0; } }
+ ///Use this to specify if the GameObject's children also use these markup settings.
public bool applyToChildren { get { return m_IgnoreChildren == 0; } set { m_IgnoreChildren = value ? 0 : 1; } }
+ ///Use this to specify which GameObject (including the GameObject’s children) the markup should be applied to.
+ ///This markup will be shared with the children of the root GameObject only if is set to true .
public Transform root { get { return InternalGetRootGO(m_EntityId); } set { m_EntityId = value != null ? value.GetEntityId() : EntityId.None; } }
int m_OverrideArea;
diff --git a/Modules/AI/Public/NavMeshBuildSettings.bindings.cs b/Modules/AI/Public/NavMeshBuildSettings.bindings.cs
index 06b57d6fbb..8fd3c6e91c 100644
--- a/Modules/AI/Public/NavMeshBuildSettings.bindings.cs
+++ b/Modules/AI/Public/NavMeshBuildSettings.bindings.cs
@@ -9,25 +9,108 @@
namespace UnityEngine.AI
{
// Keep this struct in sync with the one defined in "NavMeshBuildSettings.h"
+ ///The NavMeshBuildSettings struct allows you to specify a collection of settings which describe the dimensions and limitations of a particular agent type.
+ ///You might want to define multiple NavMeshBuildSettings if your game involves characters with large differences in height, width or climbing ability.
+ ///
+ ///You can also use this struct to control the precision and granularity of the build process, by setting the voxel and tile sizes. Some of the values are coupled, meaning there are constraints on the values based on other values. For example, it’s not valid for to be larger than .
+ ///To help diagnose violations of these rules, a special method can be evaluated.
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/AI/Public/NavMeshBuildSettings.h")]
public struct NavMeshBuildSettings
{
+ ///The agent type ID the NavMesh will be baked for.
+ ///Each or can only use a NavMesh which is built for its agent type; it is the ID that is matched.
public int agentTypeID { get { return m_AgentTypeID; } set { m_AgentTypeID = value; } }
+ ///The radius of the agent for baking in world units.
+ ///The resulting NavMesh will be shrunk by this radius to make sure that agents do not clip to walls when close to obstacles, in some scenarios it can be useful to reduce this radius.
public float agentRadius { get { return m_AgentRadius; } set { m_AgentRadius = value; } }
+ ///The height of the agent for baking in world units.
+ ///NavMesh will be removed from areas with a ceiling lower than this height. The build process does some quantization, so make sure that spaces you intend to be walkable have some extra head room.
public float agentHeight { get { return m_AgentHeight; } set { m_AgentHeight = value; } }
+ ///The maximum slope angle which is walkable (angle in degrees).
+ ///The valid range is 0–60 degrees. Steep slopes will be excluded from the resulting NavMesh. Please note that setting the slope higher than 45 can give artifacts due to the voxelization process - i.e. a steep slope cannot be distinguished from a wall.
public float agentSlope { get { return m_AgentSlope; } set { m_AgentSlope = value; } }
+ ///The maximum vertical step size an agent can take.
+ ///Must be less than agent height. This parameter is used to detect sharp discontinuities in the level (i.e. stairs, steps), and allow the agent to pass them.
public float agentClimb { get { return m_AgentClimb; } set { m_AgentClimb = value; } }
+ ///Maximum agent drop height.
+ ///Drop-Down link generation is controlled by the Drop Height parameter. The parameter controls what is the highest drop that will be connected, setting the value to 0 will disable the generation.
+ ///
+ ///The trajectory of the drop-down link is defined so that the horizontal travel is: 2 x agentRadius + 4 x voxelSize. That is, the drop will land just beyond the edge of the platform. In addition the vertical travel needs to be more than bake settings’ Step Height (otherwise we could just step down) and less than Drop Height. The adjustment by voxel size is done so that any round off errors during voxelization does not prevent the links being generated. You should set the Drop Height to a bit larger value than what you measure in your level, so that the links will connect properly.
public float ledgeDropHeight { get { return m_LedgeDropHeight; } set { m_LedgeDropHeight = value; } }
+ ///Maximum agent jump distance.
+ ///Jump-Across link generation is controlled by the Jump Distance parameter. The parameter controls what is the furthest distance that will be connected. Setting the value to 0 will disable the generation.
+ ///
+ ///The trajectory of the jump-across link is defined so that the horizontal travel is more than 2 x agentRadius and less than Jump Distance. In addition the landing location must not be further than voxelSize from the level of the start location.
public float maxJumpAcrossDistance { get { return m_MaxJumpAcrossDistance; } set { m_MaxJumpAcrossDistance = value; } }
+ ///The approximate minimum area of individual NavMesh regions.
+ ///This property allows you to cull away small non-connected NavMesh regions. NavMesh regions whose surface area is smaller than the specified value, will be removed.
+ ///
+ ///Note: some regions may not get removed. The NavMesh is built in parallel as a grid of tiles. If a region straddles a tile boundary, the region is not removed. The reason for this is that the region pruning happens at a stage in the build process where surrounding tiles are not available.
public float minRegionArea { get { return m_MinRegionArea; } set { m_MinRegionArea = value; } }
+ ///Enables overriding the default voxel size.
+ ///
public bool overrideVoxelSize { get { return m_OverrideVoxelSize != 0; } set { m_OverrideVoxelSize = value ? 1 : 0; } }
+ ///Sets the voxel size in world length units.
+ ///The NavMesh is built by first voxelizing the Scene, and then figuring out walkable spaces from the voxelized representation of the Scene. The voxel size controls how closely the NavMesh fits the geometry of your Scene, and is defined in world units.
+ ///
+ ///If you require a more detail so that the NavMesh more closely fits your Scene’s geometry, you can reduce the voxel size. An increase in detail will also cause your game to consume more memory and take more time to calculate the NavMesh data. The scaling is roughly quadratic, so doubling the resolution will result in an approximate quadrupling of the build time.
+ ///
+ ///In general you should aim to have 4-6 voxels per character diameter. For example, if you have a Scene with characters that have a radius of 0.3, a good voxel size is 0.1. The default value is set to a third of the agentRadius.
+ ///
+ ///Note: If you want to use this setting, you must also set to true.
public float voxelSize { get { return m_VoxelSize; } set { m_VoxelSize = value; } }
+ ///Enables overriding the default tile size.
+ ///
public bool overrideTileSize { get { return m_OverrideTileSize != 0; } set { m_OverrideTileSize = value ? 1 : 0; } }
+ ///Sets the tile size in voxel units.
+ ///The NavMesh is built in square tiles in order to build the mesh in parallel and to control maximum memory usage. It also helps to make the carving changes more local. If you plan to update NavMesh at runtime, a good tile size is around 32–128 voxels (roughly 5 to 20 meters for human size characters). 64 is good value to start, and you can use the [profiler window](xref:Profiler) to find a good trade off. Default value is 256, which is good for static baking. If you use a lot of carving obstacles you can try a smaller size if you see in the profiler that a lot of time is being spent on carving.
+ ///
+ ///The tile size is specified in units of voxels per tile side length.
+ ///
+ ///Note: if you want to use this setting, you must also set to true.
public int tileSize { get { return m_TileSize; } set { m_TileSize = value; } }
+ ///The maximum number of worker threads that the build process can utilize when building a NavMesh with these settings.
+ ///A value between 1 and (inclusive) causes the build process to schedule all of the work within that number of jobs when building a NavMesh. Each job computes as many NavMesh tiles as it can grab, after it has finished computing the previous tiles.
+ ///
+ ///A value of 0 or higher than JobsUtility.JobWorkerCount causes the build process to use all of the available worker threads. In this case, it computes each tile in its own separate job. The build process also computes each tile in a separate job when the number of tiles that need computing is less than the number of worker threads.
+ ///
+ ///The default value is 0.
+ ///
public uint maxJobWorkers { get { return m_MaxJobWorkers; } set { m_MaxJobWorkers = value; } }
+ ///Specifies whether to keep the NavMesh unchanged in the sections outside the build bounds during a NavMesh update.
+ ///With this property enabled, a NavMesh update recomputes only the NavMesh tiles that fall completely inside the specified local bounds. All other tiles, such as those fully outside the bounds or those that only partially intersect them, remain unchanged. Unity rebuilds the recomputed tiles as usual from the provided objects.
+ ///
+ ///The default value is false, which means all tiles are rebuilt during a NavMesh update regardless of whether they fall inside the bounds.
+ ///
+ ///This property is useful when you need to update the NavMesh in a limited volume at runtime without affecting the rest of the NavMesh. Use this property to clear tiles in a specific area. Unity removes any tile inside the bounds that has no overlapping sources.
+ ///
+ ///The spatial dimensions of a NavMesh tile equal multiplied by . The world position of the tile grid origin depends on the positions and rotations used to build and then to instantiate the . The bounds passed to or are in local space relative to that origin.
+ ///
+ ///When this property is true, a NavMesh update has the following additional effects:
+ ///
+ ///- Unity doesn't create the height mesh, and if one already exists, Unity removes it.
+ ///- Unity removes all automatically generated off-mesh links and doesn't regenerate them. This is because generated links cannot be modified for only one section of the NavMesh. Manually placed <a href="https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/api/Unity.AI.Navigation.NavMeshLink.html">NavMeshLink</a> components are unaffected.
+ ///- The NavMesh builder carries over most build settings from the existing in order to preserve consistency. The exceptions are , , , and , which are taken from the settings you provide. Unity ignores changes to other settings such as or . The values already stored in the are used instead.
+ ///
+ ///Known issue: If you call a second time for the same object, the first operation is canceled. Therefore, you can't start operations with preserveTilesOutsideBounds in parallel to affect different parts of the NavMesh. Each call must wait for the previous operation's to be true before starting.
+ ///
+ ///This property is available as of Unity 2020.1.
public bool preserveTilesOutsideBounds { get { return m_PreserveTilesOutsideBounds != 0; } set { m_PreserveTilesOutsideBounds = value ? 1 : 0; } }
+ ///Enables the creation of additional data needed to determine the height at any position on the NavMesh more accurately.
+ ///The NavMesh Agent is constrained to the surface of the NavMesh as it navigates. Since the NavMesh is an approximation of the walkable space, some features are evened out when the NavMesh is built. For example, stairs may appear as a slope in the NavMesh. If you need accurate placement of the agent for your game, enable height mesh building when you build the NavMesh. Note that building the height mesh will take up memory and processing at runtime, and it increases the time needed to bake the NavMesh.
+ ///
+ ///The current implementation of the height mesh has the following limitations:
+ ///
+ ///- It can construct height data for a Terrain only when its horizontal plane is parallel to the XZ plane of the NavMesh.
+ ///- During a NavMesh update, if the build setting "preserveTilesOutsideBounds" is true the height mesh will not be created and if it already exists, will be removed.
+ ///
+ ///This property is available as of Unity 2022.2. It will be correctly compiled in scripts when the UNITY_2022_2_OR_NEWER symbol is [defined by the engine](xref:platform-dependent-compilation).
+ ///NavMeshSurface Advanced Settings
public bool buildHeightMesh { get { return m_BuildHeightMesh != 0; } set { m_BuildHeightMesh = value ? 1 : 0; } }
+ ///Options for collecting debug data during the build process.
+ ///
+ ///
public NavMeshBuildDebugSettings debug { get { return m_Debug; } set { m_Debug = value; } }
int m_AgentTypeID;
@@ -48,6 +131,18 @@ public struct NavMeshBuildSettings
NavMeshBuildDebugSettings m_Debug;
+ ///Validates the properties of NavMeshBuildSettings.
+ ///Returns a string of violated constraints. - and suggestions for changes for the current values in the build settings and the provided bounds for building the NavMesh.
+ ///
+ ///An empty array is returned if all internal constraints are satisfied.
+ ///
+ ///Some of the settings which you can specify in the struct are coupled to each other, meaning there are constraints on the values based on other values. For example, it’s not valid for to be larger than . Another invalid case is when the vertical size of the buildBounds exceeds the height of 65535 voxel units.
+ ///
+ ///You can use this function to check if the values in violate any of the constraints, before starting the NavMesh building process.
+ ///
+ ///Ignoring the violated constraints might give unexpected results when building a NavMesh, but will still produce a NavMesh.
+ /// Describes the volume to build NavMesh for.
+ ///The list of violated constraints.
public String[] ValidationReport(Bounds buildBounds)
{
return InternalValidationReport(this, buildBounds);
@@ -60,10 +155,50 @@ public String[] ValidationReport(Bounds buildBounds)
// Consider exposing a "Validate" method to modify the BuildSettings in-place
}
+ ///Specify which of the temporary data generated while building the NavMesh should be retained in memory after the process has completed.
+ ///It is possible to collect and display in the Editor the intermediate data used in the process of building the navigation mesh using the . This can help with diagnosing those situations when the resulting NavMesh isn’t of the expected shape.
+ ///
+ ///
+ ///
+ ///Input Geometry, Regions, Polygonal Mesh Detail and Raw Contours shown after building the NavMesh with debug options
+ ///
+ ///The process for computing a NavMesh comprises of several sequential steps:
+ ///
+ ///i. decomposing the Scene's terrain and meshes into triangles;
+ ///
+ ///ii. rasterizing the input triangles into a 3D voxel representation and finding ledges;
+ ///
+ ///iii. partitioning the voxels lying at the surface into simpler horizontal regions;
+ ///
+ ///iv. finding a tight-fitting contour for each of these regions;
+ ///
+ ///v. simplifying the contours into polygonal shapes;
+ ///
+ ///vi. creating a mesh of convex polygons based on all the contours combined;
+ ///
+ ///vii. refining the polygonal mesh into a triangulated version that approximates better the Scene's original geometry.
+ ///
+ ///Through the use of the debug functionality the results from each stage can be captured and displayed separately, whereas normally they would get discarded when the NavMesh construction is completed.
+ ///
+ ///Depending on the Scene composition this debug data can be considerably large in size. It is stored in memory in a compressed manner but gets further expanded when being displayed.
+ ///
+ ///**Notes: **
+ ///
+ ///1. Unity does not save Debug visualizations - they are only available during the session in which Unity is building the NavMesh.
+ ///
+ ///2. Debug data is neither displayed nor collected when the system recomputes local patches of the NavMesh due to the presence of NavMesh Obstacles .
+ ///
+ ///
+ ///
[StructLayout(LayoutKind.Sequential)]
[NativeHeader("Modules/AI/Public/NavMeshBuildDebugSettings.h")]
public struct NavMeshBuildDebugSettings
{
+ ///Specify which types of debug data to collect when building the NavMesh.
+ ///Default value is .
+ ///
+ ///
+ ///
public NavMeshBuildDebugFlags flags { get { return (NavMeshBuildDebugFlags)m_Flags; } set { m_Flags = (byte)value; } }
byte m_Flags;
diff --git a/Modules/AIEditor/Utilities/NavMeshEditorHelpers.cs b/Modules/AIEditor/Utilities/NavMeshEditorHelpers.cs
index e746353a39..921f0ad290 100644
--- a/Modules/AIEditor/Utilities/NavMeshEditorHelpers.cs
+++ b/Modules/AIEditor/Utilities/NavMeshEditorHelpers.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
@@ -15,19 +16,22 @@ public static partial class NavMeshEditorHelpers
{
const string k_OpenAgentSettings = "NavMeshAgentInspector-OpenAgentSettings";
- internal static readonly bool isPackageInstalled;
+ [AutoStaticsCleanupOnCodeReload] // lazy cache; reset to null on reload so File.Exists re-runs on next access
+ static bool? s_IsPackageInstalled;
+ internal static bool isPackageInstalled =>
+ s_IsPackageInstalled ??= File.Exists(
+ FileUtil.PathToAbsolutePath("Packages/com.unity.ai.navigation/package.json"));
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered event handlers for agent settings navigation
internal static event Action agentTypeSettingsClicked;
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered event handlers for area settings navigation
internal static event Action areaSettingsClicked;
- static NavMeshEditorHelpers()
+ // If OpenAgentSettings() prompted the user to install the package, the install triggers a domain reload;
+ // a SessionState flag tells us to reopen the settings once the editor is ready again.
+ [OnCodeLoaded]
+ static void HandlePostInstallOpenAgentSettings()
{
- var packagePath = FileUtil.PathToAbsolutePath("Packages/com.unity.ai.navigation/package.json");
- isPackageInstalled = File.Exists(packagePath);
-
- // open the agent settings if the package was just installed via the dialog box in OpenAgentSettings()
- // there will have just been a domain reload, so a session state var is the only way to know the installation occurred from that user action
- // use a delay call to ensure SessionState API is called on the main thread, regardless of which thread NavMeshEditorHelpers is called on the first time
EditorApplication.delayCall += () =>
{
if (SessionState.GetBool(k_OpenAgentSettings, false))
diff --git a/Modules/AIEditor/Visualization/NavMeshVisualizationSettings.bindings.cs b/Modules/AIEditor/Visualization/NavMeshVisualizationSettings.bindings.cs
index d75dd5866a..67f118a962 100644
--- a/Modules/AIEditor/Visualization/NavMeshVisualizationSettings.bindings.cs
+++ b/Modules/AIEditor/Visualization/NavMeshVisualizationSettings.bindings.cs
@@ -4,6 +4,7 @@
using System;
using System.Runtime.CompilerServices;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Scripting.APIUpdating;
@@ -20,6 +21,7 @@ namespace UnityEditor.AI
public sealed class NavMeshVisualizationSettings
{
[Obsolete("showNavigation is no longer supported and will be removed.")]
+ [NoAutoStaticsCleanup] // obsolete infrastructure property; value does not affect runtime behavior
public static int showNavigation { get; set; }
internal static extern bool showOnlySelectedSurfaces { get; set; }
@@ -59,8 +61,11 @@ public static partial class NavMeshEditorHelpers
[StaticAccessor("GetNavMeshManager()", StaticAccessorType.Dot)]
internal static extern void GetAgentsDebugInfoRejectedRequestsCount(out int rejected, out int allowed);
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered debug event handlers
internal static event Action agentRejectedDebugInfoRequestsCountChanged;
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered debug event handlers
internal static event Action agentDebugRequestsPending;
+ [AutoStaticsCleanupOnCodeReload] // holds user-registered debug event handlers
internal static event Action agentDebugRequestsProcessed;
[RequiredByNativeCode]
diff --git a/Modules/Accessibility/Bindings/AccessibilityManager.bindings.cs b/Modules/Accessibility/Bindings/AccessibilityManager.bindings.cs
index 4d7225e98b..423ff69ab7 100644
--- a/Modules/Accessibility/Bindings/AccessibilityManager.bindings.cs
+++ b/Modules/Accessibility/Bindings/AccessibilityManager.bindings.cs
@@ -263,7 +263,8 @@ internal static void Internal_LateUpdate()
if (instance.m_RefreshNodeFramesRequested)
{
instance.m_RefreshNodeFramesRequested = false;
- AssistiveSupport.activeHierarchy?.RefreshNodeFrames();
+
+ AssistiveSupport.activeHierarchy?.RefreshNodeFramesWithoutResetting();
}
}
@@ -289,7 +290,7 @@ internal static int[] Internal_GetRootNodeIds()
}
[NativeHeader("Modules/Accessibility/Native/AccessibilityManager.h")]
- [FreeFunction("SetAccessibilityNodeDataPtr")]
+ [FreeFunction("SetAccessibilityNodeDataPtr", IsThreadSafe = true)]
internal extern static void SetAccessibilityNodeDataPtr(IntPtr destNodeDataPtr, AccessibilityNodeData sourceNodeData);
///
diff --git a/Modules/Accessibility/Bindings/AccessibilityNodeData.bindings.cs b/Modules/Accessibility/Bindings/AccessibilityNodeData.bindings.cs
index 2436e1fe69..a9dcb7ec56 100644
--- a/Modules/Accessibility/Bindings/AccessibilityNodeData.bindings.cs
+++ b/Modules/Accessibility/Bindings/AccessibilityNodeData.bindings.cs
@@ -105,6 +105,9 @@ public enum AccessibilityRole : byte
///- **macOS**: If this role is set on a node, the screen reader announces the node as a "search text field".
///- **Windows**: If this role is set on a node, the screen reader announces the node as "edit". The resulting behavior of this role is identical to that of .
///\\
+ /// On Android, subscribe to the event to put the search field into edit
+ /// mode when the user activates it, so that it can receive hardware keyboard input.
+ ///\\
/// On Windows and macOS, subscribe to the event to select the
/// search field represented by the node when the user navigates to it, so that it can receive keyboard input.
///
@@ -200,9 +203,9 @@ public enum AccessibilityRole : byte
/// events to perform an appropriate action when the user increases
/// or decreases the node's value, such as changing the value of the slider represented by the node. On Windows,
/// these events are only triggered for nodes whose contains a number.
- /// \\
- /// On Windows, subscribe to the event to select the slider
- /// represented by the node when the user navigates to it, so that it can receive keyboard input.
+ ///\\
+ /// On Android and Windows, subscribe to the event to select the
+ /// slider represented by the node when the user navigates to it, so that it can receive keyboard input.
///
Slider,
@@ -245,7 +248,6 @@ public enum AccessibilityRole : byte
///\\
/// This role is especially useful in:
///\\
- ///\\
///- Tab groups or sections of a user interface that need distinct boundaries.
///- Navigation bars or toolbars that contain buttons or other controls.
///- Popups, dialogs, or other temporary views.
@@ -258,7 +260,6 @@ public enum AccessibilityRole : byte
/// Container nodes themselves are not directly focusable, but they do provide the screen reader with key
/// information that enhances navigation:
///\\
- ///\\
///- They enable container navigation, which can be activated through the "Containers" <a href="https://support.google.com/accessibility/android/answer/6006598?hl=en#:~:text=Choose%20reading%20controls" >reading control</a> in TalkBack. In this navigation mode, users can move from one container to the next without having to navigate through all the nodes in between.
///- Starting with Android 14 (API level 34), the screen reader may announce when the user enters or exits a container.
///\\
@@ -274,7 +275,6 @@ public enum AccessibilityRole : byte
/// Container nodes are not directly focusable during standard screen reader navigation (called flat navigation
/// on iOS), but they provide essential context for the screen reader:
///\\
- ///\\
///- They enable container navigation, which can be activated through the "Containers" control in the <a href="https://support.apple.com/en-us/111796" >VoiceOver rotor</a>. As on Android, this navigation mode allows users to navigate efficiently between containers.
///- They enable <a href="https://support.apple.com/en-us/guide/iphone/iphfa3d32c50/ios#:~:text=Use%20flat%20or%20grouped%20navigation" >grouped navigation</a>, which can be accessed through the "Navigation Style" control in the VoiceOver rotor. In grouped navigation, container nodes are focusable. When navigating sequentially, the screen reader focuses on the container node directly instead of focusing on its child nodes. To navigate through the container's child nodes, the user must move into the container by performing a dedicated gesture. Once in a container, the user must first move out of it to navigate to nodes outside of the container. This navigation style is particularly useful in complex interfaces, where it simplifies and speeds up navigation.
///- In flat navigation, the screen reader announces the container node's label when the user enters the container by focusing on any of its child nodes. In grouped navigation, the screen reader announces the container node both when entering and when exiting it.
@@ -349,6 +349,9 @@ public enum AccessibilityRole : byte
///- **macOS**: If this role is set on a node, the screen reader announces the node as a "text".
///- **Windows**: If this role is set on a node, the screen reader announces the node as "edit".
///\\
+ /// On Android, subscribe to the event to put the text field into edit
+ /// mode when the user activates it, so that it can receive hardware keyboard input.
+ ///\\
/// On Windows and macOS, subscribe to the event to select the text
/// field represented by the node when the user navigates to it, so that it can receive keyboard input.
///
@@ -360,8 +363,7 @@ public enum AccessibilityRole : byte
///
/// **Platform-specific behavior**
///\\
- ///\\
- ///- **Android**: If this role is set on a node, the screen reader announces the node as a "dropdown list". After a short pause, it provides instructions on how to open it.
+ ///- **Android**: If this role is set on a node, the screen reader announces the node as a "button". After a short pause, it provides instructions on how to open it. If the node has set, the screen reader reads "expanded" before announcing the node's label. Otherwise, it reads "collapsed".
///- **iOS**: This role has no effect.
///- **macOS**: If this role is set on a node, the screen reader announces the node as a "pop up button". After a short pause, it provides instructions on how to open it. If the node has set, the screen reader reads "expanded" after announcing the node's label. Otherwise, it reads "collapsed".
///- **Windows**: If this role is set on a node, the screen reader announces the node as a "combo box". If the node has set, the screen reader reads "expanded" after announcing the node's label. Otherwise, it reads "collapsed".
@@ -386,7 +388,6 @@ public enum AccessibilityRole : byte
///
/// **Platform-specific behavior**
///\\
- ///\\
///- **Android**: If this role is set on a node, the screen reader announces the node as a "button". After a short pause, it provides instructions on how to activate the node.
///- **iOS**: If this role is set on a node and the node's parent has set, the screen reader announces the node as a "tab". Otherwise, it announces the node as a "button".
///- **macOS**: If this role is set on a node, the screen reader announces the node as a "tab". After a short pause, it provides instructions on how to select the node. If the node's parent has set, the screen reader also announces the tab's position in the tab bar and the total number of tabs in it.
@@ -416,7 +417,6 @@ public enum AccessibilityRole : byte
///\\
/// **Platform behavior specific to this role**
///\\
- ///\\
///- **Android**: This role does not affect the node's announcement but provides the screen reader with semantic information about the node.
///- **iOS**: This role does not have any additional effect compared to .
///- **macOS**: If this role is set on a node, the screen reader announces the node as a "scroll area".
@@ -522,11 +522,10 @@ public enum AccessibilityState : byte
///\\
/// **Notes**
///\\
- ///\\
///- This state is only supported for nodes with the role .
///- On macOS, if the role is unset from a node, the screen reader continues to announce the expanded/collapsed state of the node if its new role is compatible with this state. This is a platform limitation.
///\\
- /// **Platform support**: This state has no effect on mobile platforms.
+ /// **Platform support**: This state has no effect on iOS.
///
Expanded = 1 << 2,
}
diff --git a/Modules/Accessibility/Managed/AssistiveSupport.cs b/Modules/Accessibility/Managed/AssistiveSupport.cs
index dbd7965223..b61ec4b193 100644
--- a/Modules/Accessibility/Managed/AssistiveSupport.cs
+++ b/Modules/Accessibility/Managed/AssistiveSupport.cs
@@ -113,15 +113,7 @@ public enum ScreenReaderStatusOverride : byte
/// You can also use to determine whether the screen reader
/// is turned on or off.
///
- ///
- /// **Platform support**: This event is not supported by Narrator, the Windows built-in screen reader.
- ///
///
- ///
- /// The following example demonstrates a potential workaround for polling the status of Narrator and sending a
- /// custom event.
- ///
- ///
public static event Action screenReaderStatusChanged;
static event Action s_ActiveHierarchyChanged;
@@ -167,6 +159,17 @@ internal static event Action activeHierarchyChanged
/// When this property is set, Unity notifies the screen reader of the new hierarchy by calling
/// (with a @@null@@ parameter).
///
+ ///
+ /// **Note**: Only the accessibility hierarchy for the application's main window is supported. Content displayed
+ /// on additional windows, such as on secondary displays, is not exposed to screen readers.
+ ///
+ ///
+ /// **Warning**: Assigning a hierarchy builds its native representation, and setting this property to @@null@@
+ /// tears it down. This has a non-trivial cost on the following platforms:
+ ///\\
+ ///- **iOS**: Switching the active hierarchy has a high cost that scales with the size of the hierarchy. Avoid setting this property frequently in performance-sensitive code.
+ ///- **macOS**: Switching the active hierarchy has a moderate cost that scales with the size of the hierarchy. Doing so frequently can affect performance.
+ ///
///
public static AccessibilityHierarchy activeHierarchy
{
diff --git a/Modules/Accessibility/Managed/Hierarchy/AccessibilityHierarchy.cs b/Modules/Accessibility/Managed/Hierarchy/AccessibilityHierarchy.cs
index d75386c114..15a35a97e1 100644
--- a/Modules/Accessibility/Managed/Hierarchy/AccessibilityHierarchy.cs
+++ b/Modules/Accessibility/Managed/Hierarchy/AccessibilityHierarchy.cs
@@ -60,6 +60,9 @@ namespace UnityEngine.Accessibility
///-
///-
///
+ /// **Note**: Only the accessibility hierarchy for the application's main window is supported. Content displayed on
+ /// additional windows, such as on secondary displays, is not exposed to screen readers.
+ ///
/// SA:
///
///- [[wiki:accessibility|Accessibility for mobile applications]]
@@ -111,27 +114,21 @@ internal event Action changed
internal static int nextUniqueNodeId;
///
- /// The set of all node IDs currently in use across all instances of .
- /// Used to guarantee global ID uniqueness even after wraps around
- /// .
+ /// Weak references to every that has been created and not yet garbage
+ /// collected. The live hierarchies are the source of truth for which node IDs are currently in use, which lets
+ /// guarantee global ID uniqueness even after wraps
+ /// around . Using weak references means a hierarchy that is simply dropped (for
+ /// example during a scene transition) frees its IDs as soon as it is collected, with no finalizer or explicit
+ /// cleanup required.
///
- internal static readonly HashSet usedNodeIds = new();
+ static readonly List> s_LiveHierarchies = new();
///
- /// Finalizer that releases all node IDs back to the global pool when this hierarchy is garbage collected
- /// without having been explicitly cleared. Without this, IDs would accumulate in
- /// permanently for every hierarchy that is created and then simply dropped (e.g. during scene transitions or
- /// in tests), eventually exhausting all available IDs.
+ /// Initializes and returns an empty .
///
- ~AccessibilityHierarchy()
+ public AccessibilityHierarchy()
{
- lock (usedNodeIds)
- {
- foreach (var id in nodes.Keys)
- {
- usedNodeIds.Remove(id);
- }
- }
+ s_LiveHierarchies.Add(new WeakReference(this));
}
///
@@ -245,8 +242,13 @@ public AccessibilityNode InsertNode(int childIndex, string label = null, Accessi
/// different position in the parent's child list.
///
///
- /// **Warning**: The moving operation is costly because many checks have to be executed to guarantee the
- /// integrity of the hierarchy. If this method is called excessively, it might negatively affect performance.
+ /// **Warning**: When this hierarchy is the , this method has a
+ /// non-trivial cost on the following platforms:
+ ///\\
+ ///- **iOS**: Moving a node has a high cost that scales with the size of the subtree being moved. Avoid calling this method frequently in performance-sensitive code.
+ ///- **macOS**: Moving a node has a moderate cost. Calling this method frequently can affect performance.
+ ///\\
+ /// Moving a node in an inactive hierarchy has no native cost.
///
/// The node to move.
/// The new parent of the node, or @@null@@ if the node should be placed at the root
@@ -313,6 +315,15 @@ public bool MoveNode(AccessibilityNode node, AccessibilityNode newParent, int ne
///
/// Removes the node from the accessibility hierarchy and removes or re-parents its descendants.
///
+ ///
+ /// **Warning**: When this hierarchy is the , this method has a
+ /// non-trivial cost on the following platforms:
+ ///\\
+ ///- **iOS**: Removing a node has a high cost that scales with the size of the affected subtree. Avoid calling this method frequently in performance-sensitive code.
+ ///- **macOS**: Removing a node has a moderate cost that scales with the size of the affected subtree. Calling this method frequently can affect performance.
+ ///\\
+ /// Removing a node from an inactive hierarchy has no native cost.
+ ///
/// The node to remove.
/// @@true@@ if the node's descendants should also be removed, or @@false@@ if they
/// should be moved under the node's parent. Defaults to @@true@@.
@@ -333,12 +344,9 @@ public void RemoveNode(AccessibilityNode node, bool removeChildren = true)
if (removeChildren)
{
- var nodeIdsToRemove = new List();
-
void RemoveFromNodes(AccessibilityNode child)
{
nodes.Remove(child.id);
- nodeIdsToRemove.Add(child.id);
foreach (var descendant in child.children)
{
@@ -347,23 +355,10 @@ void RemoveFromNodes(AccessibilityNode child)
}
RemoveFromNodes(node);
-
- lock (usedNodeIds)
- {
- foreach (var nodeId in nodeIdsToRemove)
- {
- usedNodeIds.Remove(nodeId);
- }
- }
}
else
{
nodes.Remove(node.id);
-
- lock (usedNodeIds)
- {
- usedNodeIds.Remove(node.id);
- }
}
if (m_RootNodes.Contains(node))
@@ -385,6 +380,15 @@ void RemoveFromNodes(AccessibilityNode child)
///
/// Resets the hierarchy to an empty state, removing all nodes and the screen reader focus.
///
+ ///
+ /// **Warning**: When this hierarchy is the , this method has a
+ /// non-trivial cost on the following platforms:
+ ///\\
+ ///- **iOS**: Clearing the hierarchy has a high cost that scales with its size. Avoid calling this method frequently in performance-sensitive code.
+ ///- **macOS**: Clearing the hierarchy has a moderate cost that scales with its size. Calling this method frequently can affect performance.
+ ///\\
+ /// Clearing an inactive hierarchy has no native cost.
+ ///
public void Clear()
{
for (var i = m_RootNodes.Count - 1; i >= 0; i--)
@@ -399,14 +403,18 @@ public void Clear()
///
///
/// This is a convenience method that updates the of all nodes in the
- /// accessibility hierarchy (based on ) and notifies the screen
- /// reader of these updates by calling
+ /// accessibility hierarchy based on and notifies the screen reader
+ /// of these updates by calling
/// (with a @@null@@ parameter).
///
///
/// Call this method when most or all of the nodes on the screen require a layout update. For example, when the
/// user scrolls the application's interface, or when the orientation of the screen changes.
///
+ ///
+ /// **Note**: For nodes with no set, this method resets their
+ /// to .
+ ///
///
public void RefreshNodeFrames()
{
@@ -421,6 +429,21 @@ public void RefreshNodeFrames()
}
}
+ internal void RefreshNodeFramesWithoutResetting()
+ {
+ foreach (var node in nodes.Values)
+ {
+ // Set the frame even if it is the same, because it needs to be converted to screen coordinates on the
+ // native side, which could be different if the app window was moved, for example.
+ node.frame = node.frameGetter?.Invoke() ?? node.frame;
+ }
+
+ if (AssistiveSupport.activeHierarchy == this)
+ {
+ AssistiveSupport.notificationDispatcher.SendLayoutChanged();
+ }
+ }
+
///
/// Retrieves the lowest common ancestor of two nodes in the accessibility hierarchy.
///
@@ -503,37 +526,56 @@ void BuildNodeIdStack(AccessibilityNode node, ref Stack nodeS
/// The new node.
AccessibilityNode CreateNode()
{
- // Guard and select the next free ID. Both the count check and the Contains loop must run inside
- // the lock so that a concurrent finalizer Remove() cannot corrupt the HashSet while we read it.
- int nodeId;
+ var startId = nextUniqueNodeId;
- lock (usedNodeIds)
+ // Skip over any IDs that are still in use by a node in a live hierarchy. This is important after
+ // nextUniqueNodeId wraps around int.MaxValue.
+ while (IsNodeIdInUse(nextUniqueNodeId))
{
- if (usedNodeIds.Count >= int.MaxValue)
+ nextUniqueNodeId = nextUniqueNodeId == int.MaxValue ? 0 : nextUniqueNodeId + 1;
+
+ // If we cycle all the way back to where we started, every possible ID is in use and we cannot create
+ // a new node.
+ if (nextUniqueNodeId == startId)
{
throw new InvalidOperationException("Could not create a new accessibility node. A maximum of " +
$"{int.MaxValue} nodes can exist at a time across all hierarchies. Try clearing unused " +
"hierarchies or removing unused nodes.");
}
+ }
- // Skip over any IDs that are still in use by nodes in any hierarchy. This is important after
- // nextUniqueNodeId wraps around int.MaxValue.
- while (usedNodeIds.Contains(nextUniqueNodeId))
- {
- nextUniqueNodeId = nextUniqueNodeId == int.MaxValue ? 0 : nextUniqueNodeId + 1;
- }
+ // Reserve the ID.
+ var nodeId = nextUniqueNodeId;
- // Reserve the ID.
- nodeId = nextUniqueNodeId;
+ // Loop the counter. We do not expect to have that many accessibility nodes at the same time.
+ nextUniqueNodeId = nodeId == int.MaxValue ? 0 : nodeId + 1;
- // Mark this ID as in use.
- usedNodeIds.Add(nodeId);
+ return new AccessibilityNode(nodeId, this);
+ }
- // Loop the counter. We do not expect to have that many accessibility nodes at the same time.
- nextUniqueNodeId = nodeId == int.MaxValue ? 0 : nodeId + 1;
+ ///
+ /// Determines whether the given node ID is currently in use by a node in any live hierarchy. Dead weak
+ /// references encountered along the way are pruned from .
+ ///
+ static bool IsNodeIdInUse(int id)
+ {
+ for (var i = s_LiveHierarchies.Count - 1; i >= 0; i--)
+ {
+ if (s_LiveHierarchies[i].TryGetTarget(out var hierarchy))
+ {
+ if (hierarchy.nodes.ContainsKey(id))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ // The hierarchy has been garbage collected, so its IDs are free again. Prune the dead reference.
+ s_LiveHierarchies.RemoveAt(i);
+ }
}
- return new AccessibilityNode(nodeId, this);
+ return false;
}
AccessibilityNode CreateNodeAndSetParent(int childIndex, string label, AccessibilityNode parent)
diff --git a/Modules/Accessibility/Managed/Hierarchy/AccessibilityNode.cs b/Modules/Accessibility/Managed/Hierarchy/AccessibilityNode.cs
index 2d3ed9aeca..73d450e50d 100644
--- a/Modules/Accessibility/Managed/Hierarchy/AccessibilityNode.cs
+++ b/Modules/Accessibility/Managed/Hierarchy/AccessibilityNode.cs
@@ -341,11 +341,11 @@ public string label
///-
///-
///
- /// On Windows, nodes with the role must have
- /// a value containing a number between 0 and 100 to accurately communicate the scroll percentage
- /// to the screen reader. For scroll views that support both vertical and horizontal scrolling, the value
- /// must contain two numbers, with the vertical scroll percentage listed first.
- /// For example, a value of `50, 75` indicates that the scroll view represented by the node is scrolled 50% vertically and 75% horizontally.
+ /// On Windows, nodes with the role must have a value containing a
+ /// number between 0 and 100 to accurately communicate the scroll percentage to the screen reader. For scroll
+ /// views that support both vertical and horizontal scrolling, the value must contain two numbers, with the
+ /// vertical scroll percentage listed first. For example, a value of `50, 75` indicates that the scroll view
+ /// represented by the node is scrolled 50% vertically and 75% horizontally.
///
///
public string value
diff --git a/Modules/AdaptivePerformance/AssemblyInfo.cs b/Modules/AdaptivePerformance/AssemblyInfo.cs
index e42ce3fa86..e3540765ad 100644
--- a/Modules/AdaptivePerformance/AssemblyInfo.cs
+++ b/Modules/AdaptivePerformance/AssemblyInfo.cs
@@ -10,4 +10,8 @@
[assembly: InternalsVisibleTo("Unity.Modules.AdaptivePerformanceEditor.Tests.TestPackage.Editor")]
[assembly: InternalsVisibleTo("Unity.Modules.AdaptivePerformanceEditor.Tests.Playmode")]
+// Lets the Adaptive Performance package's Visual Scripting bridge forward internal events
+// (e.g. AdaptivePerformanceIndexer.ScalerLevelChanged) to the Visual Scripting EventBus.
+[assembly: InternalsVisibleTo("Unity.AdaptivePerformance")]
+
diff --git a/Modules/AdaptivePerformance/Core/AdaptivePerformanceInit.cs b/Modules/AdaptivePerformance/Core/AdaptivePerformanceInit.cs
index b19b3bad0e..68446dab0b 100644
--- a/Modules/AdaptivePerformance/Core/AdaptivePerformanceInit.cs
+++ b/Modules/AdaptivePerformance/Core/AdaptivePerformanceInit.cs
@@ -2,12 +2,14 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Scripting;
namespace UnityEngine.AdaptivePerformance
{
- internal static class AdaptivePerformanceInitializer
+ internal static partial class AdaptivePerformanceInitializer
{
+ [AutoStaticsCleanupOnCodeReload]
static AdaptivePerformanceManagerSpawner s_Spawner;
[RequiredByNativeCode(optional: false)]
diff --git a/Modules/AdaptivePerformance/Core/AdaptiverPerformanceLog.cs b/Modules/AdaptivePerformance/Core/AdaptiverPerformanceLog.cs
index 9fc45a97b2..ca7187607d 100644
--- a/Modules/AdaptivePerformance/Core/AdaptiverPerformanceLog.cs
+++ b/Modules/AdaptivePerformance/Core/AdaptiverPerformanceLog.cs
@@ -3,12 +3,15 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System.Text;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEngine.AdaptivePerformance
{
internal static class APLog
{
+ [NoAutoStaticsCleanup] // logging toggle, set externally
public static bool enabled = false;
+ [NoAutoStaticsCleanup] // reusable string-building buffer, populated fresh each log call
public static readonly StringBuilder s_LogBuilder = new StringBuilder(512);
static readonly string s_AdaptivePerformancePrefix = "[Adaptive Performance] ";
public static void Debug(string format, params object[] args)
diff --git a/Modules/AdaptivePerformance/IAdaptivePerformance.cs b/Modules/AdaptivePerformance/IAdaptivePerformance.cs
index 348bd25add..67a804a8b3 100644
--- a/Modules/AdaptivePerformance/IAdaptivePerformance.cs
+++ b/Modules/AdaptivePerformance/IAdaptivePerformance.cs
@@ -3,6 +3,7 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System.ComponentModel;
+using Unity.Scripting.LifecycleManagement;
namespace UnityEngine.AdaptivePerformance
{
@@ -197,8 +198,9 @@ public interface IAdaptivePerformance
/// }
/// ]]>
///
- public static class Holder
+ public static partial class Holder
{
+ [AutoStaticsCleanupOnCodeReload]
static IAdaptivePerformance m_Instance;
///
@@ -371,6 +373,7 @@ public static void Deinitialize()
/// }
///
///
+ [AutoStaticsCleanupOnCodeReload]
public static event LifecycleEventHandler LifecycleEventHandler;
}
diff --git a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceIndexer.cs b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceIndexer.cs
index 30ad9dd0ee..e4eb1eb1e6 100644
--- a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceIndexer.cs
+++ b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceIndexer.cs
@@ -241,6 +241,16 @@ public class AdaptivePerformanceIndexer
///
public StateAction PerformanceAction { get; private set; }
+ // Raised when the indexer changes a registered scaler's level (increase or decrease).
+ // Internal so the Visual Scripting bridge in the Adaptive Performance package can forward it
+ // to the Visual Scripting EventBus without expanding the public engine API.
+ internal event System.Action ScalerLevelChanged;
+
+ internal void NotifyScalerLevelChanged(AdaptivePerformanceScaler scaler)
+ {
+ ScalerLevelChanged?.Invoke(scaler);
+ }
+
///
/// Returns all currently applied scalers.
///
diff --git a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceRenderSettings.cs b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceRenderSettings.cs
index 26904b5ca0..e5b421362d 100644
--- a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceRenderSettings.cs
+++ b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceRenderSettings.cs
@@ -4,6 +4,7 @@
using System.Collections;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
namespace UnityEngine.AdaptivePerformance
@@ -14,9 +15,13 @@ namespace UnityEngine.AdaptivePerformance
///
public static class AdaptivePerformanceRenderSettings
{
+ [NoAutoStaticsCleanup] // render quality multiplier actively managed by the scaler system
private static float s_MaxShadowDistanceMultiplier = 1;
+ [NoAutoStaticsCleanup] // render quality multiplier actively managed by the scaler system
private static float s_ShadowResolutionMultiplier = 1;
+ [NoAutoStaticsCleanup] // render quality multiplier actively managed by the scaler system
private static float s_RenderScaleMultiplier = 1;
+ [NoAutoStaticsCleanup] // render quality value actively managed by the scaler system
private static float s_DecalsMaxDistance = 1000;
///
@@ -40,6 +45,7 @@ public static float DecalsDrawDistance
///
/// Adjust the number of shadow cascades for the main camera in the scene.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static int MainLightShadowCascadesCountBias
{
get;
@@ -49,6 +55,7 @@ public static int MainLightShadowCascadesCountBias
///
/// Adjust the quality setting of shadows.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static int ShadowQualityBias
{
get;
@@ -57,6 +64,7 @@ public static int ShadowQualityBias
///
/// Adjust the size of lookup tables that are used for color grading.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static float LutBias
{
get;
@@ -81,6 +89,7 @@ public static float RenderScaleMultiplier
///
/// Adjust the quality of MSAA.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static int AntiAliasingQualityBias
{
get;
@@ -102,6 +111,7 @@ public static bool SkipDynamicBatching
/// When enabled, there is a higher load on the CPU but less rendering overdraw.
/// When disabled, there is less CPU utilization but more overdraw.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static bool SkipFrontToBackSorting
{
get;
@@ -112,6 +122,7 @@ public static bool SkipFrontToBackSorting
/// Whether transparent objects should be rendered
/// When enabled, there is less rendering overdraw, but entire objects can disappear.
///
+ [NoAutoStaticsCleanup] // render quality setting actively managed by the scaler system
public static bool SkipTransparentObjects
{
get;
diff --git a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceScaler.cs b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceScaler.cs
index ba6405e81a..3ffd471dea 100644
--- a/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceScaler.cs
+++ b/Modules/AdaptivePerformance/Indexer/AdaptivePerformanceScaler.cs
@@ -4,7 +4,6 @@
using UnityEngine.Scripting;
-
namespace UnityEngine.AdaptivePerformance
{
///
@@ -319,6 +318,7 @@ internal void IncreaseLevel()
CurrentLevel++;
OnLevelIncrease();
OnLevel();
+ m_Indexer?.NotifyScalerLevelChanged(this);
}
internal void DecreaseLevel()
@@ -331,6 +331,7 @@ internal void DecreaseLevel()
CurrentLevel--;
OnLevelDecrease();
OnLevel();
+ m_Indexer?.NotifyScalerLevelChanged(this);
}
internal void Activate()
diff --git a/Modules/AdaptivePerformance/Management/AdaptivePerformanceGeneralSettings.cs b/Modules/AdaptivePerformance/Management/AdaptivePerformanceGeneralSettings.cs
index 9ac60abdab..b3dcbd6b40 100644
--- a/Modules/AdaptivePerformance/Management/AdaptivePerformanceGeneralSettings.cs
+++ b/Modules/AdaptivePerformance/Management/AdaptivePerformanceGeneralSettings.cs
@@ -2,6 +2,7 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Bindings;
using UnityEngine.Scripting;
@@ -10,10 +11,12 @@ namespace UnityEngine.AdaptivePerformance
///
/// A `ScriptableObject` that contains global settings applicable to all Adaptive Performance providers.
///
- public class AdaptivePerformanceGeneralSettings : ScriptableObject
+ public partial class AdaptivePerformanceGeneralSettings : ScriptableObject
{
/// The key used to query to get the current loader settings.
+ [NoAutoStaticsCleanup] // public API field; readonly would be a source-breaking change
public static string k_SettingsKey = "com.unity.adaptiveperformance.loader_settings";
+ [AutoStaticsCleanupOnCodeReload]
internal static AdaptivePerformanceGeneralSettings s_RuntimeSettingsInstance = null;
[SerializeField]
diff --git a/Modules/AdaptivePerformance/Management/AdaptivePerformanceManagerSettings.cs b/Modules/AdaptivePerformance/Management/AdaptivePerformanceManagerSettings.cs
index e821306edc..23bf6297b0 100644
--- a/Modules/AdaptivePerformance/Management/AdaptivePerformanceManagerSettings.cs
+++ b/Modules/AdaptivePerformance/Management/AdaptivePerformanceManagerSettings.cs
@@ -6,6 +6,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using Unity.Scripting.LifecycleManagement;
[assembly: InternalsVisibleTo("Unity.AdaptivePerformance.Tests")]
[assembly: InternalsVisibleTo("Unity.AdaptivePerformance.Editor.Tests")]
@@ -37,7 +38,7 @@ namespace UnityEngine.AdaptivePerformance
/// * OnDisable calls internally. Ask the active loader to stop all subsystems.
/// * OnDestroy calls internally. Deinitialize and remove the active loader.
///
- public sealed class AdaptivePerformanceManagerSettings : ScriptableObject
+ public sealed partial class AdaptivePerformanceManagerSettings : ScriptableObject
{
[HideInInspector]
bool m_InitializationComplete = false;
@@ -99,6 +100,7 @@ public bool isInitializationComplete
}
[HideInInspector]
+ [AutoStaticsCleanupOnCodeReload]
static AdaptivePerformanceLoader s_ActiveLoader = null;
///
diff --git a/Modules/AdaptivePerformance/Management/IAdaptivePerformanceSettings.cs b/Modules/AdaptivePerformance/Management/IAdaptivePerformanceSettings.cs
index 5eaacaf14b..a21e3b5f0c 100644
--- a/Modules/AdaptivePerformance/Management/IAdaptivePerformanceSettings.cs
+++ b/Modules/AdaptivePerformance/Management/IAdaptivePerformanceSettings.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
+using Unity.Scripting.LifecycleManagement;
//using UnityEditor;
using UnityEngine;
using UnityEngine.Bindings;
@@ -555,6 +556,7 @@ public IReadOnlyList DefaultScalerSetting
}
}
+ [NoAutoStaticsCleanup] // fixed compile-time list of built-in scaler types, never changes
internal static readonly List k_DefaultScalerNames = new List
{
typeof(UnityEngine.AdaptivePerformance.AdaptiveFramerate),
diff --git a/Modules/AdaptivePerformance/Profiler/AdaptivePerformanceProfilerStats.cs b/Modules/AdaptivePerformance/Profiler/AdaptivePerformanceProfilerStats.cs
index 28778b59e4..55783688f8 100644
--- a/Modules/AdaptivePerformance/Profiler/AdaptivePerformanceProfilerStats.cs
+++ b/Modules/AdaptivePerformance/Profiler/AdaptivePerformanceProfilerStats.cs
@@ -10,6 +10,7 @@
using Unity.Profiling;
using Unity.Profiling.LowLevel;
using Unity.Profiling.LowLevel.Unsafe;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Profiling;
using Unity.Collections.LowLevel.Unsafe;
@@ -84,62 +85,77 @@ private static byte GetProfilerMarkerDataType()
///
/// Profiler counter to report cpu frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CurrentCPUMarker = new CustomProfilerMarker("CPU frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report cpu average frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker AvgCPUMarker = new CustomProfilerMarker("CPU avg frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report gpu frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CurrentGPUMarker = new CustomProfilerMarker("GPU frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report gpu average frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker AvgGPUMarker = new CustomProfilerMarker("GPU avg frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report cpu performance level.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CurrentCPULevelMarker = new CustomProfilerMarker("CPU performance level", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report gpu performance level.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CurrentGPULevelMarker = new CustomProfilerMarker("GPU performance level", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CurrentFrametimeMarker = new CustomProfilerMarker("Frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report average frametime.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker AvgFrametimeMarker = new CustomProfilerMarker("Avg frametime", ProfilerMarkerDataUnit.TimeNanoseconds);
///
/// Profiler counter to report the thermal warning level.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker WarningLevelMarker = new CustomProfilerMarker("Thermal Warning Level", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the temperature level.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker TemperatureLevelMarker = new CustomProfilerMarker("Temperature Level", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the temperature trend.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker TemperatureTrendMarker = new CustomProfilerMarker("Temperature Trend", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the bottleneck.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker BottleneckMarker = new CustomProfilerMarker("Bottleneck", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the performance mode.
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker PerformanceModeMarker = new CustomProfilerMarker("Performance Mode", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the CPU utilization (normalized 0-1).
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker CpuUtilizationMarker = new CustomProfilerMarker("CPU Utilization", ProfilerMarkerDataUnit.Count);
///
/// Profiler counter to report the GPU utilization (normalized 0-1).
///
+ [NoAutoStaticsCleanup] // public API field; profiler counter initialized once at startup
public static CustomProfilerMarker GpuUtilizationMarker = new CustomProfilerMarker("GPU Utilization", ProfilerMarkerDataUnit.Count);
///
@@ -154,7 +170,9 @@ private static byte GetProfilerMarkerDataType()
const int kMaxScalerNameSizeInBytes = 320;
+ [NoAutoStaticsCleanup] // per-frame profiler buffer, populated fresh each frame
static List scalerInfos = new List();
+ [NoAutoStaticsCleanup] // per-frame profiler buffer index, populated fresh each frame
static Dictionary scalerInfosIndex = new Dictionary();
///
diff --git a/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderLoader.cs b/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderLoader.cs
index fdf10a55ea..2a85358c11 100644
--- a/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderLoader.cs
+++ b/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderLoader.cs
@@ -4,6 +4,7 @@
using UnityEngine;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.AdaptivePerformance;
using UnityEngine.AdaptivePerformance.Basic;
using UnityEngine.AdaptivePerformance.Provider;
@@ -18,6 +19,7 @@ namespace UnityEngine.AdaptivePerformance.Basic
[VisibleToOtherModules("UnityEditor.AdaptivePerformanceModule")]
internal class BasicProviderLoader : AdaptivePerformanceLoaderHelper
{
+ [NoAutoStaticsCleanup] // populated by native subsystem registration, not C# state
static List s_BasicSubsystemDescriptors =
new List();
diff --git a/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderSettings.cs b/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderSettings.cs
index 97aec36077..c7b58331ed 100644
--- a/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderSettings.cs
+++ b/Modules/AdaptivePerformance/Provider/BasicProvider/BasicProviderSettings.cs
@@ -2,6 +2,7 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.AdaptivePerformance;
@@ -12,8 +13,9 @@ namespace UnityEngine.AdaptivePerformance.Basic
///
[System.Serializable]
[AdaptivePerformanceConfigurationData("Basic", BasicProviderConstants.k_SettingKey)]
- public class BasicProviderSettings: IAdaptivePerformanceSettings
+ public partial class BasicProviderSettings: IAdaptivePerformanceSettings
{
+ [AutoStaticsCleanupOnCodeReload]
static BasicProviderSettings m_Instance = null;
void Awake()
{
diff --git a/Modules/AdaptivePerformance/Scalers/AdaptiveResolution.cs b/Modules/AdaptivePerformance/Scalers/AdaptiveResolution.cs
index a64791cb29..cb512aab3b 100644
--- a/Modules/AdaptivePerformance/Scalers/AdaptiveResolution.cs
+++ b/Modules/AdaptivePerformance/Scalers/AdaptiveResolution.cs
@@ -2,6 +2,7 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.Rendering;
namespace UnityEngine.AdaptivePerformance
@@ -12,6 +13,7 @@ namespace UnityEngine.AdaptivePerformance
///
public class AdaptiveResolution : AdaptivePerformanceScaler
{
+ [NoAutoStaticsCleanup] // instance counter maintained via Start/OnDestroy lifecycle
static int instanceCount = 0;
///
diff --git a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfo.cs b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfo.cs
index 7bb3d36da4..46b3881850 100644
--- a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfo.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfo.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
-
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.AdaptivePerformance;
namespace UnityEditor.AdaptivePerformance.Editor
@@ -37,7 +37,7 @@ public bool Equals(AdaptivePerformanceLoaderInfo other)
return other != null && Equals(loaderType, other.loaderType) && Equals(instance, other.instance);
}
- static string[] s_LoaderblockList = { "DummyLoader", "SampleLoader", "AdaptivePerformanceLoaderHelper" };
+ static readonly string[] s_LoaderblockList = { "DummyLoader", "SampleLoader", "AdaptivePerformanceLoaderHelper" };
internal static void GetAllKnownLoaderInfos(List newInfos)
{
diff --git a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfoManager.cs b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfoManager.cs
index f811cca5ef..4d30caf74f 100644
--- a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfoManager.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceLoaderInfoManager.cs
@@ -5,20 +5,23 @@
using System;
using System.Collections.Generic;
using System.IO;
-
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.AdaptivePerformance;
namespace UnityEditor.AdaptivePerformance.Editor
{
- internal class AdaptivePerformanceLoaderInfoManager : IAdaptivePerformanceLoaderOrderManager
+ internal partial class AdaptivePerformanceLoaderInfoManager : IAdaptivePerformanceLoaderOrderManager
{
// Simple class to give us updates when the asset database changes.
- internal class AssetCallbacks : AssetPostprocessor
+ internal partial class AssetCallbacks : AssetPostprocessor
{
+ [AutoStaticsCleanup]
static bool s_EditorUpdatable = false;
+ [AutoStaticsCleanup]
internal static System.Action Callback { get; set; }
- static AssetCallbacks()
+ [OnCodeLoaded]
+ static void Initialize()
{
if (!s_EditorUpdatable)
{
diff --git a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceSettingsManager.cs b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceSettingsManager.cs
index 412096ee00..831b0dd22f 100644
--- a/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceSettingsManager.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/AdaptivePerformanceSettingsManager.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.AdaptivePerformance.Editor.Metadata;
using UnityEditor.PackageManager;
using UnityEngine;
@@ -13,9 +14,10 @@
namespace UnityEditor.AdaptivePerformance.Editor
{
- class AdaptivePerformanceSettingsManager : SettingsProvider
+ partial class AdaptivePerformanceSettingsManager : SettingsProvider
{
const string k_WarningPlaymodePopup = "Adaptive Performance settings cannot be changed while the Editor is in Play mode.";
+ const string k_WarningPackageOperationPopup = "Adaptive Performance is updating its packages. Settings are temporarily unavailable until the operation finishes.";
struct Content
{
@@ -26,13 +28,21 @@ struct Content
public static readonly GUIContent k_FrameTimingExplanatoryText = new GUIContent(L10n.Tr("Please enable Frame Timing Stats in the Player Settings. Adaptive Performance requires precise frame time information."));
}
- internal static string s_SettingsRootTitle = $"Project/{AdaptivePerformanceConstants.kAdaptivePerformanceProviderManagement}";
+ internal static readonly string s_SettingsRootTitle = $"Project/{AdaptivePerformanceConstants.kAdaptivePerformanceProviderManagement}";
+ [AutoStaticsCleanup]
static AdaptivePerformanceSettingsManager s_SettingsManager = null;
+ [NoAutoStaticsCleanup] // UI state flag, refreshed from settings on each OnGUI call
static bool s_EnableAdaptivePerformance = false;
internal static AdaptivePerformanceSettingsManager Instance => s_SettingsManager;
+ // Guards the get-or-create of the general settings config object. A dedicated object rather than
+ // Instance: the provider instance is null until Create() runs (and is cleared again on statics
+ // cleanup), so locking on it both threw ArgumentNullException and changed identity across reloads.
+ [NoAutoStaticsCleanup]
+ static readonly object s_CurrentSettingsLock = new object();
+
static AdaptivePerformanceGeneralSettingsPerBuildTarget currentSettings
{
get
@@ -41,7 +51,7 @@ static AdaptivePerformanceGeneralSettingsPerBuildTarget currentSettings
EditorBuildSettings.TryGetConfigObject(AdaptivePerformanceGeneralSettings.k_SettingsKey, out generalSettings);
if (generalSettings == null && s_EnableAdaptivePerformance)
{
- lock (AdaptivePerformanceSettingsManager.Instance)
+ lock (s_CurrentSettingsLock)
{
EditorBuildSettings.TryGetConfigObject(AdaptivePerformanceGeneralSettings.k_SettingsKey, out generalSettings);
if (generalSettings == null)
@@ -314,7 +324,9 @@ void DisplayEnableToggle()
if (EditorApplication.isPlayingOrWillChangePlaymode)
EditorGUILayout.HelpBox(L10n.Tr(k_WarningPlaymodePopup), MessageType.Warning);
- using (new EditorGUI.DisabledScope(EditorApplication.isPlayingOrWillChangePlaymode))
+ else if (EditorUtilities.IsPackageOperationInProgress)
+ EditorGUILayout.HelpBox(L10n.Tr(k_WarningPackageOperationPopup), MessageType.Info);
+ using (new EditorGUI.DisabledScope(EditorApplication.isPlayingOrWillChangePlaymode || EditorUtilities.IsPackageOperationInProgress))
{
s_EnableAdaptivePerformance = EditorGUILayout.Toggle("Enable Adaptive Performance", s_EnableAdaptivePerformance);
}
diff --git a/Modules/AdaptivePerformanceEditor/Management/EditorUtilities.cs b/Modules/AdaptivePerformanceEditor/Management/EditorUtilities.cs
index a0feb553b9..871bd1a3cb 100644
--- a/Modules/AdaptivePerformanceEditor/Management/EditorUtilities.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/EditorUtilities.cs
@@ -7,14 +7,17 @@
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.Build.Profile;
using UnityEditor.PackageManager;
+using UnityEditor.PackageManager.Requests;
using UnityEngine;
using UnityEngine.AdaptivePerformance;
using UnityEngine.Assemblies;
namespace UnityEditor.AdaptivePerformance.Editor
{
+ [InitializeOnLoad]
internal static class EditorUtilities
{
internal static readonly string[] s_DefaultGeneralSettingsPath = {"Adaptive Performance"};
@@ -431,20 +434,89 @@ internal static AdaptivePerformanceGeneralSettings GetSettingsOrBuildProfilesSet
return settings;
}
+ // Toggling Adaptive Performance adds or removes the built-in module package, which triggers an
+ // asynchronous package resolution and domain reload. Interacting with the settings or entering Play
+ // mode while that operation is in flight corrupts state (mismatched DefaultScenario state, duplicate
+ // ScriptableSingletons, missing script references), so we track the pending operation to gate the
+ // settings UI and to veto Play mode entry until it completes.
+ // Resetting this on a Play mode transition would drop the gate while the operation is still resolving,
+ // which is exactly what it exists to prevent. MonitorPackageRequest clears it on completion, and the
+ // package operation itself ends in a domain reload that discards it.
+ [NoAutoStaticsCleanup]
+ static Func s_PendingPackageOperation;
+
+ const string k_WarningPlaymodeDuringPackageOperation =
+ "Adaptive Performance is updating its packages. Wait for the operation to finish before entering Play mode.";
+
+ static EditorUtilities()
+ {
+ EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
+ }
+
+ // True while an Adaptive Performance enable/disable package operation (and its domain reload) is still resolving.
+ internal static bool IsPackageOperationInProgress => s_PendingPackageOperation?.Invoke() ?? false;
+
+ static void OnPlayModeStateChanged(PlayModeStateChange state)
+ {
+ // ExitingEditMode is the last point at which Play mode entry can be cancelled. Block it while a
+ // package operation is still resolving to avoid entering Play mode mid domain reload.
+ if (state != PlayModeStateChange.ExitingEditMode || !IsPackageOperationInProgress)
+ return;
+
+ Debug.LogWarning(L10n.Tr(k_WarningPlaymodeDuringPackageOperation));
+ EditorApplication.isPlaying = false;
+ }
+
internal static void EnableAPModule(bool enable)
{
var packageID = "com.unity.modules.adaptiveperformance";
if (enable)
{
- Client.Add(packageID);
+ TrackPackageRequest(Client.Add(packageID));
}
else
{
var settings = EditorUtilities.GetSettingsOrBuildProfilesSettings();
if (settings == null)
- Client.Remove(packageID); // neither classic platforms (Settings) nor build profiles are enabled
+ TrackPackageRequest(Client.Remove(packageID)); // neither classic platforms (Settings) nor build profiles are enabled
}
}
+
+ internal static void TrackPackageRequest(Request request)
+ {
+ // Gate the settings UI on the request until UPM reports it complete.
+ TrackPackageOperation(() => request != null && !request.IsCompleted);
+ }
+
+ // Tracks an arbitrary "still in progress" predicate. Separated from TrackPackageRequest so the
+ // gating and cleanup logic can be exercised without a live UnityEditor.PackageManager request,
+ // whose completion state cannot be faked.
+ internal static void TrackPackageOperation(Func isInProgress)
+ {
+ s_PendingPackageOperation = isInProgress;
+ EditorApplication.update -= MonitorPackageRequest;
+ EditorApplication.update += MonitorPackageRequest;
+ }
+
+ internal static void MonitorPackageRequest()
+ {
+ bool inProgress;
+ try
+ {
+ inProgress = IsPackageOperationInProgress;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ inProgress = false;
+ }
+
+ if (inProgress)
+ return;
+
+ s_PendingPackageOperation = null;
+ EditorApplication.update -= MonitorPackageRequest;
+ }
}
}
diff --git a/Modules/AdaptivePerformanceEditor/Management/EditorWorkQueue.cs b/Modules/AdaptivePerformanceEditor/Management/EditorWorkQueue.cs
index f6814f4180..e738113364 100644
--- a/Modules/AdaptivePerformanceEditor/Management/EditorWorkQueue.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/EditorWorkQueue.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
-
+using Unity.Scripting.LifecycleManagement;
using UnityEditor;
using UnityEngine;
@@ -22,7 +22,7 @@ internal static bool SessionStateHasStoredData(string queueName)
}
}
- internal class EditorWorkQueue : EditorWorkQueueBase
+ internal partial class EditorWorkQueue : EditorWorkQueueBase
{
[Serializable]
struct Queue
@@ -33,6 +33,7 @@ struct Queue
public string QueueName { get; set; }
+ [AutoStaticsCleanup]
private static Lazy> s_Instance = new Lazy>();
public static EditorWorkQueue Instance => s_Instance.Value;
diff --git a/Modules/AdaptivePerformanceEditor/Management/EnterNamePopup.cs b/Modules/AdaptivePerformanceEditor/Management/EnterNamePopup.cs
index d4e142707a..3047a764d4 100644
--- a/Modules/AdaptivePerformanceEditor/Management/EnterNamePopup.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/EnterNamePopup.cs
@@ -16,8 +16,8 @@ class EnterNamePopup : PopupWindowContent
private string m_NewProfileName = "New Scaler Profile";
private bool m_NeedsFocus = true;
private List existingProfileNames = new List();
- static string s_WarningPopup = L10n.Tr("Warning");
- static string s_WarningPopupOption = L10n.Tr("Ok");
+ static readonly string s_WarningPopup = L10n.Tr("Warning");
+ static readonly string s_WarningPopupOption = L10n.Tr("Ok");
const int k_maxChars = 256;
static readonly string k_ErrorMessageLength = string.Format(L10n.Tr("Scaler profile name is too long (maximum {0} characters)"), k_maxChars);
diff --git a/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformanceKnownPackages.cs b/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformanceKnownPackages.cs
index 071984b3cf..558e2c14d3 100644
--- a/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformanceKnownPackages.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformanceKnownPackages.cs
@@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
-
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
namespace UnityEditor.AdaptivePerformance.Editor.Metadata
{
- internal class AdaptivePerformanceKnownPackages
+ internal partial class AdaptivePerformanceKnownPackages
{
class KnownLoaderMetadata : IAdaptivePerformanceLoaderMetadata
{
@@ -36,6 +36,7 @@ class KnownPackage : IAdaptivePerformancePackage
public bool PopulateNewSettingsInstance(ScriptableObject obj) { return true; }
}
+ [AutoStaticsCleanup]
private static Lazy> s_KnownPackages = new Lazy>(InitKnownPackages);
internal static List Packages => s_KnownPackages.Value;
@@ -177,6 +178,29 @@ static List InitKnownPackages()
}
}
});
+
+ packages.Add(new KnownPackage() {
+ metadata = new KnownPackageMetadata(){
+ packageName = "Adaptive Performance Meta OpenXR",
+ packageId = "com.unity.xr.meta-openxr",
+ settingsType = "UnityEngine.XR.OpenXR.Features.Meta.MetaOpenXRAdaptivePerformanceProviderSettings",
+ licenseURL = "https://docs.unity3d.com/Packages/com.unity.xr.meta-openxr@latest?subfolder=/license/LICENSE.html",
+ isDefaultPlatformProvider = "false",
+ isDeprecated = "false",
+ loaderMetadata = new List()
+ {
+ new KnownLoaderMetadata() {
+ loaderName = "Meta OpenXR Provider",
+ loaderType = "UnityEngine.XR.OpenXR.Features.Meta.MetaOpenXRAdaptivePerformanceProviderLoader",
+ supportedBuildTargets = new List()
+ {
+ BuildTargetGroup.Android
+ },
+ priority = 2,
+ },
+ }
+ }
+ });
return packages;
}
}
diff --git a/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformancePackageMetadata.cs b/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformancePackageMetadata.cs
index 02a0df0e9d..303c54440b 100644
--- a/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformancePackageMetadata.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/Metadata/AdaptivePerformancePackageMetadata.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
@@ -113,7 +114,7 @@ public interface IAdaptivePerformancePackageMetadata
/// Provide access to the metadata store. Currently only usable as a way to assign and remove loaders
/// to or from an instance.
///
- public class AdaptivePerformancePackageMetadataStore
+ public partial class AdaptivePerformancePackageMetadataStore
{
const string k_WaitingPackmanQuery = "APMGT Waiting Packman Query.";
const string k_RebuildCache = "APMGT Rebuilding Cache.";
@@ -122,8 +123,10 @@ public class AdaptivePerformancePackageMetadataStore
const string k_UninstallingPackage = "APMGT Uninstalling Adaptive Performance Package.";
const string k_CachedMDStoreKey = "Adaptive Performance Metadata Store";
- static float k_TimeOutDelta = 30f;
+ const float k_TimeOutDelta = 30f;
+ [AutoStaticsCleanup]
static bool s_KnowPackageInitialized = false;
+ [AutoStaticsCleanup]
static bool s_EnableLogging = false;
@@ -144,6 +147,7 @@ struct CachedMDStoreInformation
public string[] installablePackages;
}
+ [AutoStaticsCleanup]
static CachedMDStoreInformation s_CachedMDStoreInformation = new CachedMDStoreInformation()
{
hasAlreadyRequestedData = false,
@@ -221,8 +225,11 @@ struct LoaderAssignmentRequests
public List activeRequests;
}
+ [AutoStaticsCleanup]
static List m_AddRequests = new List();
+ [AutoStaticsCleanup]
static Dictionary s_Packages = new Dictionary();
+ [AutoStaticsCleanup]
static SearchRequest s_SearchRequest = null;
const string k_DefaultSessionStateString = "AP_DEFAULT_SESSION_STATE";
@@ -566,7 +573,8 @@ internal static void InitKnownPluginPackages()
}
}
- static AdaptivePerformancePackageMetadataStore()
+ [OnCodeLoaded]
+ static void Initialize()
{
EditorApplication.playModeStateChanged += PlayModeStateChanged;
if (IsEditorInPlayMode())
diff --git a/Modules/AdaptivePerformanceEditor/Management/ProviderSettingsEditor.cs b/Modules/AdaptivePerformanceEditor/Management/ProviderSettingsEditor.cs
index 4a67a18279..c3e226744d 100644
--- a/Modules/AdaptivePerformanceEditor/Management/ProviderSettingsEditor.cs
+++ b/Modules/AdaptivePerformanceEditor/Management/ProviderSettingsEditor.cs
@@ -29,47 +29,47 @@ public class ProviderSettingsEditor : UnityEditor.Editor
const string k_IndexerPerformanceActionDelay = "m_PerformanceActionDelay";
const string k_ScalerProfileList = "m_scalerProfileList";
- static GUIContent s_LoggingLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Logging"), L10n.Tr("Only active in development mode."));
- static GUIContent s_AutomaticPerformanceModeEnabledLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Auto Performance Mode"), L10n.Tr("Auto Performance Mode controls performance by changing CPU and GPU levels."));
- static GUIContent s_AutomaticGameModeEnabledLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Auto Game Mode"), L10n.Tr("Auto Game Mode controls performance by changing target FPS based on device GameMode settings."));
- static GUIContent s_EnableBoostOnStartupLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Boost mode on startup"), L10n.Tr("Enables the CPU and GPU boost mode before engine startup to decrease startup time."));
- static GUIContent s_StatsLoggingFrequencyInFramesLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Logging Frequency"), L10n.Tr("Changes the logging frequency."));
- static GUIContent s_IndexerActiveLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Active"), L10n.Tr("Is indexer enabled."));
- static GUIContent s_IndexerThermalActionDelayLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Thermal Action Delay"), L10n.Tr("Delay after any scaler is applied or unapplied because of thermal state."));
- static GUIContent s_IndexerPerformanceActionDelayLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Performance Action Delay"), L10n.Tr("Delay after any scaler is applied or unapplied because of performance state."));
-
- static GUIContent s_ScalerScale = EditorGUIUtility.TrTextContent(L10n.Tr("Scale"), L10n.Tr("Scale to control the quality impact for the scaler. No quality change when 1, improved quality when >1, and lowered quality when <1"));
- static GUIContent s_ScalerVisualImpact = EditorGUIUtility.TrTextContent(L10n.Tr("Visual Impact"), L10n.Tr("Visual impact the scaler has on the application. The higher the more impact the scaler has on the visuals."));
- static GUIContent s_ScalerTarget = EditorGUIUtility.TrTextContent(L10n.Tr("Target"), L10n.Tr("Target for the scaler of the application bottleneck. The target selected has the most impact on the quality control of this scaler. Can only be overriden via API."));
- static GUIContent s_ScalerMaxLevel = EditorGUIUtility.TrTextContent(L10n.Tr("Max Level"), L10n.Tr("Maximum level for the scaler. This is tied to the implementation of the scaler to divide the levels into concrete steps."));
- static GUIContent s_ScalerMinBound = EditorGUIUtility.TrTextContent(L10n.Tr("Min Scale"), L10n.Tr("Minimum value for the scale boundary."));
- static GUIContent s_ScalerMaxBound = EditorGUIUtility.TrTextContent(L10n.Tr("Max Scale"), L10n.Tr("Maximum value for the scale boundary."));
-
- static GUIContent s_AdaptiveFramerate = EditorGUIUtility.TrTextContent(L10n.Tr("Framerate"), L10n.Tr("Adaptive Framerate enables you to automatically control the application's framerate by the defined minimum and maximum framerate. It uses Application.targetFramerate to control the framerate for your application."));
- static GUIContent s_AdaptiveResolution = EditorGUIUtility.TrTextContent(L10n.Tr("Resolution"), L10n.Tr("Adaptive Resolution enables you to automatically control the screen resolution of the application by the defined scale. It uses Dynamic Resolution (Vulkan only) and uses Resolution Scale of the Universal Render Pipeline as fallback if the project uses Universal Render Pipeline."));
- static GUIContent s_AdaptiveLOD = EditorGUIUtility.TrTextContent(L10n.Tr("LOD"), L10n.Tr("Adaptive LOD changes the LOD bias based on the thermal and performance load."));
- static GUIContent s_AdaptiveLut = EditorGUIUtility.TrTextContent(L10n.Tr("LUT"), L10n.Tr("Requires Universal Render Pipeline. Adaptive LUT changes the LUT Bias of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveMSAA = EditorGUIUtility.TrTextContent(L10n.Tr("MSAA"), L10n.Tr("Requires Universal Render Pipeline. Adaptive MSAA changes the Anti Aliasing Quality Bias of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveShadowCascade = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Cascade"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Cascade changes the Main Light Shadow Cascades Count Bias of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveShadowDistance = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Distance"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Distance changes the Max Shadow Distance Multiplier of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveShadowmapResolution = EditorGUIUtility.TrTextContent(L10n.Tr("Shadowmap Resolution"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadowmap Resolution changes the Main Light Shadowmap Resolution Multiplier of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveShadowQuality = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Quality"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Quality changes the Shadow Quality Bias of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveSorting = EditorGUIUtility.TrTextContent(L10n.Tr("Sorting"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Sorting skips the front-to-back sorting of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveTransparency = EditorGUIUtility.TrTextContent(L10n.Tr("Transparency"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Transparency skips transparent objects render pass."));
- static GUIContent s_AdaptiveViewDistance = EditorGUIUtility.TrTextContent(L10n.Tr("View Distance"), L10n.Tr("Adaptive View Distance changes the view distance of the main camera. Requires the MainCamera tag on the Camera you want to assign."));
- static GUIContent s_AdaptivePhysics = EditorGUIUtility.TrTextContent(L10n.Tr("Physics"), L10n.Tr("Adaptive Physics changes the Time.fixedDeltaTime based on the thermal and performance load."));
- static GUIContent s_AdaptiveDecals = EditorGUIUtility.TrTextContent(L10n.Tr("Decals"), L10n.Tr("Adaptive Decal changes the maximum draw distance for all decals of the Universal Render Pipeline based on the thermal and performance load."));
- static GUIContent s_AdaptiveLayerCulling = EditorGUIUtility.TrTextContent(L10n.Tr("Layer Culling"), L10n.Tr("Adaptive Layer Culling changes the maximum draw distance for each layer based on the thermal and performance load. It scales the value provided by camera.layerCullDistances."));
-
- static string s_FramerateWarningVSync = L10n.Tr("Adaptive Framerate is only supported without VSync. Set VSync Count to \"Don't Sync\" in Quality settings.");
- static string s_FramerateWarningGameMode = L10n.Tr("Adaptive Framerate is only supported when \"Auto Game Mode\" is turned off.");
- static string s_WarningPopup = L10n.Tr("Warning");
- static string s_WarningPopupMessage = L10n.Tr("Adaptive Performance requires at least one profile to work properly");
- static string s_WarningPopupOption = L10n.Tr("Ok");
- static string s_AdaptiveFramerateMenu = L10n.Tr("Adaptive Framerate");
- static string s_WarningPlaymodePopup = L10n.Tr("Adaptive Performance settings cannot be changed when the Editor is in Play mode.");
- static string s_WarningIndexer = L10n.Tr("You have to enable Adaptive Performance Indexer to use Scaler.");
- static string s_WarningLegacyPackage = L10n.Tr(" Please consider update the legacy provider settings editor to support build profile UI properly. ");
+ static readonly GUIContent s_LoggingLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Logging"), L10n.Tr("Only active in development mode."));
+ static readonly GUIContent s_AutomaticPerformanceModeEnabledLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Auto Performance Mode"), L10n.Tr("Auto Performance Mode controls performance by changing CPU and GPU levels."));
+ static readonly GUIContent s_AutomaticGameModeEnabledLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Auto Game Mode"), L10n.Tr("Auto Game Mode controls performance by changing target FPS based on device GameMode settings."));
+ static readonly GUIContent s_EnableBoostOnStartupLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Boost mode on startup"), L10n.Tr("Enables the CPU and GPU boost mode before engine startup to decrease startup time."));
+ static readonly GUIContent s_StatsLoggingFrequencyInFramesLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Logging Frequency"), L10n.Tr("Changes the logging frequency."));
+ static readonly GUIContent s_IndexerActiveLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Active"), L10n.Tr("Is indexer enabled."));
+ static readonly GUIContent s_IndexerThermalActionDelayLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Thermal Action Delay"), L10n.Tr("Delay after any scaler is applied or unapplied because of thermal state."));
+ static readonly GUIContent s_IndexerPerformanceActionDelayLabel = EditorGUIUtility.TrTextContent(L10n.Tr("Performance Action Delay"), L10n.Tr("Delay after any scaler is applied or unapplied because of performance state."));
+
+ static readonly GUIContent s_ScalerScale = EditorGUIUtility.TrTextContent(L10n.Tr("Scale"), L10n.Tr("Scale to control the quality impact for the scaler. No quality change when 1, improved quality when >1, and lowered quality when <1"));
+ static readonly GUIContent s_ScalerVisualImpact = EditorGUIUtility.TrTextContent(L10n.Tr("Visual Impact"), L10n.Tr("Visual impact the scaler has on the application. The higher the more impact the scaler has on the visuals."));
+ static readonly GUIContent s_ScalerTarget = EditorGUIUtility.TrTextContent(L10n.Tr("Target"), L10n.Tr("Target for the scaler of the application bottleneck. The target selected has the most impact on the quality control of this scaler. Can only be overriden via API."));
+ static readonly GUIContent s_ScalerMaxLevel = EditorGUIUtility.TrTextContent(L10n.Tr("Max Level"), L10n.Tr("Maximum level for the scaler. This is tied to the implementation of the scaler to divide the levels into concrete steps."));
+ static readonly GUIContent s_ScalerMinBound = EditorGUIUtility.TrTextContent(L10n.Tr("Min Scale"), L10n.Tr("Minimum value for the scale boundary."));
+ static readonly GUIContent s_ScalerMaxBound = EditorGUIUtility.TrTextContent(L10n.Tr("Max Scale"), L10n.Tr("Maximum value for the scale boundary."));
+
+ static readonly GUIContent s_AdaptiveFramerate = EditorGUIUtility.TrTextContent(L10n.Tr("Framerate"), L10n.Tr("Adaptive Framerate enables you to automatically control the application's framerate by the defined minimum and maximum framerate. It uses Application.targetFramerate to control the framerate for your application."));
+ static readonly GUIContent s_AdaptiveResolution = EditorGUIUtility.TrTextContent(L10n.Tr("Resolution"), L10n.Tr("Adaptive Resolution enables you to automatically control the screen resolution of the application by the defined scale. It uses Dynamic Resolution (Vulkan only) and uses Resolution Scale of the Universal Render Pipeline as fallback if the project uses Universal Render Pipeline."));
+ static readonly GUIContent s_AdaptiveLOD = EditorGUIUtility.TrTextContent(L10n.Tr("LOD"), L10n.Tr("Adaptive LOD changes the LOD bias based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveLut = EditorGUIUtility.TrTextContent(L10n.Tr("LUT"), L10n.Tr("Requires Universal Render Pipeline. Adaptive LUT changes the LUT Bias of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveMSAA = EditorGUIUtility.TrTextContent(L10n.Tr("MSAA"), L10n.Tr("Requires Universal Render Pipeline. Adaptive MSAA changes the Anti Aliasing Quality Bias of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveShadowCascade = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Cascade"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Cascade changes the Main Light Shadow Cascades Count Bias of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveShadowDistance = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Distance"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Distance changes the Max Shadow Distance Multiplier of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveShadowmapResolution = EditorGUIUtility.TrTextContent(L10n.Tr("Shadowmap Resolution"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadowmap Resolution changes the Main Light Shadowmap Resolution Multiplier of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveShadowQuality = EditorGUIUtility.TrTextContent(L10n.Tr("Shadow Quality"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Shadow Quality changes the Shadow Quality Bias of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveSorting = EditorGUIUtility.TrTextContent(L10n.Tr("Sorting"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Sorting skips the front-to-back sorting of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveTransparency = EditorGUIUtility.TrTextContent(L10n.Tr("Transparency"), L10n.Tr("Requires Universal Render Pipeline. Adaptive Transparency skips transparent objects render pass."));
+ static readonly GUIContent s_AdaptiveViewDistance = EditorGUIUtility.TrTextContent(L10n.Tr("View Distance"), L10n.Tr("Adaptive View Distance changes the view distance of the main camera. Requires the MainCamera tag on the Camera you want to assign."));
+ static readonly GUIContent s_AdaptivePhysics = EditorGUIUtility.TrTextContent(L10n.Tr("Physics"), L10n.Tr("Adaptive Physics changes the Time.fixedDeltaTime based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveDecals = EditorGUIUtility.TrTextContent(L10n.Tr("Decals"), L10n.Tr("Adaptive Decal changes the maximum draw distance for all decals of the Universal Render Pipeline based on the thermal and performance load."));
+ static readonly GUIContent s_AdaptiveLayerCulling = EditorGUIUtility.TrTextContent(L10n.Tr("Layer Culling"), L10n.Tr("Adaptive Layer Culling changes the maximum draw distance for each layer based on the thermal and performance load. It scales the value provided by camera.layerCullDistances."));
+
+ static readonly string s_FramerateWarningVSync = L10n.Tr("Adaptive Framerate is only supported without VSync. Set VSync Count to \"Don't Sync\" in Quality settings.");
+ static readonly string s_FramerateWarningGameMode = L10n.Tr("Adaptive Framerate is only supported when \"Auto Game Mode\" is turned off.");
+ static readonly string s_WarningPopup = L10n.Tr("Warning");
+ static readonly string s_WarningPopupMessage = L10n.Tr("Adaptive Performance requires at least one profile to work properly");
+ static readonly string s_WarningPopupOption = L10n.Tr("Ok");
+ static readonly string s_AdaptiveFramerateMenu = L10n.Tr("Adaptive Framerate");
+ static readonly string s_WarningPlaymodePopup = L10n.Tr("Adaptive Performance settings cannot be changed when the Editor is in Play mode.");
+ static readonly string s_WarningIndexer = L10n.Tr("You have to enable Adaptive Performance Indexer to use Scaler.");
+ static readonly string s_WarningLegacyPackage = L10n.Tr(" Please consider update the legacy provider settings editor to support build profile UI properly. ");
SerializedProperty m_LoggingProperty;
SerializedProperty m_AutoPerformanceModeEnabledProperty;
@@ -125,11 +125,11 @@ public class ProviderSettingsEditor : UnityEditor.Editor
///
protected virtual bool IsThermalActionDelayAvailable { get; private set; } = true;
- static GUIContent k_ShowRuntimeSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Runtime Settings"));
- static GUIContent k_ShowDevelopmentSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Development Settings"));
- static GUIContent k_ShowIndexerSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Indexer Settings"));
- static GUIContent k_ShowScalerSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Scaler Settings"));
- static GUIContent k_ShowScalerProfiles = EditorGUIUtility.TrTextContent(L10n.Tr("Scaler Profiles"));
+ static readonly GUIContent k_ShowRuntimeSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Runtime Settings"));
+ static readonly GUIContent k_ShowDevelopmentSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Development Settings"));
+ static readonly GUIContent k_ShowIndexerSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Indexer Settings"));
+ static readonly GUIContent k_ShowScalerSettings = EditorGUIUtility.TrTextContent(L10n.Tr("Scaler Settings"));
+ static readonly GUIContent k_ShowScalerProfiles = EditorGUIUtility.TrTextContent(L10n.Tr("Scaler Profiles"));
struct ScalerSettingInformation
{
@@ -142,8 +142,8 @@ class ScalerProfileSettingInformation
public Dictionary scalerSettingsInfos = new Dictionary();
}
- static int k_NumberOfScalerProperties = 5;
- static int k_TickboxPosition = 227;
+ const int k_NumberOfScalerProperties = 5;
+ const int k_TickboxPosition = 227;
Dictionary m_ScalerProfiles = new Dictionary();
@@ -193,7 +193,7 @@ void AddNewReorderableList(List list)
///
public void OnEnable()
{
- if (serializedObject == null || serializedObject.targetObject == null)
+ if (target == null || serializedObject == null || serializedObject.targetObject == null)
return;
m_FoldoutState.Clear();
m_FieldObjects.Clear();
@@ -228,7 +228,7 @@ public void OnEnable()
///
public bool DisplayBaseSettingsBegin(bool isLegacyAPI = true)
{
- if (serializedObject == null || serializedObject.targetObject == null)
+ if (target == null || serializedObject == null || serializedObject.targetObject == null)
return false;
serializedObject.Update();
@@ -279,7 +279,7 @@ public bool DisplayBaseSettingsBegin(bool isLegacyAPI = true)
///
public void DisplayBaseSettingsEnd(bool isLegacyAPI = true)
{
- if (serializedObject == null || serializedObject.targetObject == null)
+ if (target == null || serializedObject == null || serializedObject.targetObject == null)
return;
if (isLegacyAPI)
diff --git a/Modules/AdaptivePerformanceEditor/Profiler/AdaptivePerformanceProfilerModule.cs b/Modules/AdaptivePerformanceEditor/Profiler/AdaptivePerformanceProfilerModule.cs
index f1bda7af46..f50400495a 100644
--- a/Modules/AdaptivePerformanceEditor/Profiler/AdaptivePerformanceProfilerModule.cs
+++ b/Modules/AdaptivePerformanceEditor/Profiler/AdaptivePerformanceProfilerModule.cs
@@ -5,6 +5,7 @@
using System;
using Unity.Profiling;
using Unity.Profiling.Editor;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.AdaptivePerformance;
namespace UnityEditor.AdaptivePerformance.Editor
{
@@ -12,6 +13,7 @@ namespace UnityEditor.AdaptivePerformance.Editor
[ProfilerModuleMetadata("Adaptive Performance")]
internal class AdaptivePerformanceProfilerModule : ProfilerModule
{
+ [NoAutoStaticsCleanup] // fixed compile-time list of profiler counter descriptors
static readonly ProfilerCounterDescriptor[] k_ChartCounters = new ProfilerCounterDescriptor[]
{
new ProfilerCounterDescriptor("CPU frametime", ProfilerCategory.Scripts),
diff --git a/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderLoader.cs b/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderLoader.cs
index 7a19eac268..b79b62d7ad 100644
--- a/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderLoader.cs
+++ b/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderLoader.cs
@@ -4,6 +4,7 @@
using UnityEngine;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEngine.AdaptivePerformance;
using UnityEditor.AdaptivePerformance.Editor;
using UnityEngine.AdaptivePerformance.Provider;
@@ -16,6 +17,7 @@ namespace UnityEditor.AdaptivePerformance.Simulator.Editor
[AdaptivePerformanceSupportedBuildTargetAttribute(BuildTargetGroup.Standalone)]
public class SimulatorProviderLoader : AdaptivePerformanceLoaderHelper
{
+ [NoAutoStaticsCleanup] // populated by native subsystem registration, not C# state
static List s_SimulatorSubsystemDescriptors =
new List();
diff --git a/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderSettings.cs b/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderSettings.cs
index f2f408651e..5f24e685da 100644
--- a/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderSettings.cs
+++ b/Modules/AdaptivePerformanceEditor/Provider/SimulatorProviderSettings.cs
@@ -2,6 +2,7 @@
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+using Unity.Scripting.LifecycleManagement;
using UnityEngine;
using UnityEngine.AdaptivePerformance;
@@ -12,8 +13,9 @@ namespace UnityEditor.AdaptivePerformance.Simulator.Editor
///
[System.Serializable]
[AdaptivePerformanceConfigurationData("Simulator", SimulatorProviderConstants.k_SettingsKey)]
- public class SimulatorProviderSettings : IAdaptivePerformanceSettings
+ public partial class SimulatorProviderSettings : IAdaptivePerformanceSettings
{
+ [AutoStaticsCleanup]
static SimulatorProviderSettings m_Settings = null;
///
diff --git a/Modules/AdaptivePerformanceEditor/UI/BuildProfileAdaptivePerformanceProviderUI.cs b/Modules/AdaptivePerformanceEditor/UI/BuildProfileAdaptivePerformanceProviderUI.cs
index a35e1b9b73..63fe78ce6b 100644
--- a/Modules/AdaptivePerformanceEditor/UI/BuildProfileAdaptivePerformanceProviderUI.cs
+++ b/Modules/AdaptivePerformanceEditor/UI/BuildProfileAdaptivePerformanceProviderUI.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Scripting.LifecycleManagement;
using UnityEditor.AdaptivePerformance.Editor.Metadata;
using UnityEditor.Build.Profile;
using UnityEditor.PackageManager;
@@ -36,6 +37,7 @@ internal struct LoaderInformation
DropdownField m_DropDown;
List m_LoaderNameList;
ReorderableList m_ReorderableList;
+ [NoAutoStaticsCleanup] // dialog-guard flag, always restored to false after dialog closes
static bool s_ShowingDialog = false;
internal readonly GUIContent k_ViewGuide = new("View Guide");
diff --git a/Modules/AnimationWindow/Editor/AddCurvesPopupHierarchyGUI.cs b/Modules/AnimationWindow/Editor/AddCurvesPopupHierarchyGUI.cs
index a6c256c918..ca2c77ca74 100644
--- a/Modules/AnimationWindow/Editor/AddCurvesPopupHierarchyGUI.cs
+++ b/Modules/AnimationWindow/Editor/AddCurvesPopupHierarchyGUI.cs
@@ -16,9 +16,7 @@ internal class AddCurvesPopupHierarchyGUI : TreeViewGUI
{
public EditorWindow owner;
public bool showPlusButton { get; set; }
- private GUIStyle buttonStyle = "IconButton";
private GUIContent plusIcon = EditorGUIUtility.TrIconContent("Toolbar Plus");
- private GUIStyle plusButtonBackgroundStyle = "Tag MenuItem";
private GUIContent addPropertiesContent = EditorGUIUtility.TrTextContent("Add Properties");
private const float plusButtonWidth = 17;
@@ -42,14 +40,14 @@ private void DoAddCurveButton(Rect rowRect, TreeViewItem node)
if (hierarchyNode == null || hierarchyNode.curveBindings == null || hierarchyNode.curveBindings.Length == 0)
return;
- Rect buttonRect = new Rect(rowRect.width - plusButtonWidth, rowRect.yMin, plusButtonWidth, buttonStyle.fixedHeight);
+ Rect buttonRect = new Rect(rowRect.width - plusButtonWidth, rowRect.yMin, plusButtonWidth, AnimationWindowStyles.plusButton.fixedHeight);
// TODO Make a style for add curves popup
// Draw background behind plus button to prevent text overlapping
- GUI.Box(buttonRect, GUIContent.none, plusButtonBackgroundStyle);
+ GUI.Box(buttonRect, GUIContent.none, AnimationWindowStyles.plusButtonBackground);
// Check if the curve already exists and remove plus button
- if (GUI.Button(buttonRect, plusIcon, buttonStyle))
+ if (GUI.Button(buttonRect, plusIcon, AnimationWindowStyles.plusButton))
{
AddCurvesPopup.AddNewCurve(hierarchyNode);
diff --git a/Modules/AnimationWindow/Editor/AnimationWindow.cs b/Modules/AnimationWindow/Editor/AnimationWindow.cs
index 6becd6aad3..69665c449b 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindow.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindow.cs
@@ -186,6 +186,9 @@ void OnEnable()
m_AnimEditor.hideFlags = HideFlags.HideAndDontSave;
}
+ if (state != null)
+ state.onHierarchySelectionChange += SyncSceneSelection;
+
s_AnimationWindows.Add(this);
OnSelectionChangeInternal(false);
@@ -340,6 +343,96 @@ internal void OnSelectionUpdated()
state?.OnSelectionUpdated();
}
+ internal bool WouldChangeSelection(UnityObject candidate)
+ {
+ if (m_LockTracker.isLocked || (state != null && state.linkedWithSequencer))
+ return false;
+
+ foreach (var responder in s_Responders)
+ {
+ if (WouldResponderChangeSelection(responder, candidate))
+ return true;
+ }
+ return false;
+ }
+
+ bool WouldResponderChangeSelection(IAnimationWindowResponder responder, UnityEngine.Object candidateObject)
+ {
+ if (!responder.OnSelectionChange(this, candidateObject, out var probe))
+ return false;
+ if (probe == selection)
+ return false;
+ if (probe != null)
+ probe.Dispose();
+ return true;
+ }
+
+ // Set scene active go to be the same as the one selected from hierarchy
+ void SyncSceneSelection(int[] selectedNodeIDs)
+ {
+ if (state == null || state.filterBySelection)
+ return;
+
+ var selection = state.selection;
+ if (selection == null || !selection.canSyncSceneSelection)
+ return;
+
+ GameObject rootGameObject = selection.rootGameObject;
+ if (rootGameObject == null)
+ return;
+
+ var selectedGameObjectIDs = new List(selectedNodeIDs.Length);
+ foreach (var selectedNodeID in selectedNodeIDs)
+ {
+ // Skip nodes without associated curves.
+ if (selectedNodeID == 0)
+ continue;
+
+ AnimationWindowHierarchyNode node = state.hierarchyData.FindItem(selectedNodeID) as AnimationWindowHierarchyNode;
+
+ if (node == null)
+ continue;
+
+ if (node is AnimationWindowHierarchyMasterNode)
+ continue;
+
+ Transform t = rootGameObject.transform.Find(node.path);
+
+ // In the case of nested animation component, we don't want to sync the scene selection (case 569506)
+ // When selection changes, animation window will always pick nearest animator component in terms of hierarchy depth
+ // Automatically syncinc scene selection in nested scenarios would cause unintuitive clip & animation change for animation window so we check for it and deny sync if necessary
+ if (selection.IsCompatibleWith(t))
+ {
+ EntityId entity;
+ if (node.curves.Length > 0)
+ {
+ AnimationWindowCurve firstCurve = node.curves[0];
+ // Query the animation system for the associate EntityId
+ // For custom IAnimationBinding (e.g., UIToolkit), this will return the appropriate selection object
+ // For standard animations, returns the GameObject's EntityId
+ entity = AnimationUtility.GetAssociatedEntityId(t.gameObject, firstCurve.binding);
+
+ // Case 569506 protection extended to non-Animator routing: skip when the
+ // entity routes to a different SelectionItem (e.g. nested VE with its own UIAnimationClip).
+ var entityObj = EditorUtility.EntityIdToObject(entity);
+ if (entityObj != null && entityObj != t.gameObject && WouldChangeSelection(entityObj))
+ continue;
+ }
+ else
+ {
+ entity = t.gameObject.GetEntityId();
+ }
+
+ selectedGameObjectIDs.Add(entity);
+ }
+ }
+
+ if (selectedGameObjectIDs.Count > 0)
+ Selection.entityIds = selectedGameObjectIDs.ToArray();
+ else
+ Selection.activeGameObject = rootGameObject;
+ }
+
void OnFocus()
{
OnSelectionChangeInternal(false);
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowHierarchyGUI.cs b/Modules/AnimationWindow/Editor/AnimationWindowHierarchyGUI.cs
index 922edefc38..906124b60d 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowHierarchyGUI.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowHierarchyGUI.cs
@@ -43,8 +43,9 @@ internal class AnimationWindowHierarchyGUI : TreeViewGUI
private const float k_ValueFieldDragWidth = 15;
private const float k_ValueFieldWidth = 80;
private const float k_ValueFieldOffsetFromRightSide = 100;
- private const float k_ObjectFieldAdditionalWidth = 30;
- private const float k_ObjectFieldAdditionalOffset = k_ObjectFieldAdditionalWidth + 30;
+ private const float k_ObjectFieldWidth = 110;
+ private const float k_ObjectFieldOffsetFromRightSide = 160;
+ private const float k_LabelFieldSpacing = 2;
private const float k_ObjectFieldMaxHeight = 18;
private const float k_ColorIndicatorTopMargin = 3;
public static readonly float k_DopeSheetRowHeight = EditorGUI.kSingleLineHeight;
@@ -110,6 +111,8 @@ protected void DoNodeGUI(Rect rect, AnimationWindowHierarchyNode node, bool sele
float indent = k_BaseIndent + (node.depth + node.indent) * k_IndentWidth;
+ CalculateRects(rect, node, indent, out var labelRect, out var valueFieldRect);
+
if (node is AnimationWindowHierarchyAddButtonNode)
{
if (Event.current.type == EventType.MouseMove && s_WasInsideValueRectFrame >= 0)
@@ -128,13 +131,13 @@ protected void DoNodeGUI(Rect rect, AnimationWindowHierarchyNode node, bool sele
else
{
DoRowBackground(rect, row);
- DoIconAndName(rect, node, selected, focused, indent);
+ DoIconAndName(rect, labelRect, node, selected, focused);
DoFoldout(node, rect, indent, row);
bool enabled = !state.selection.isReadOnly;
using (new EditorGUI.DisabledScope(!enabled))
{
- DoValueField(rect, node, row);
+ DoValueField(rect, valueFieldRect, node, row);
}
DoCurveDropdown(rect, node, row, enabled);
HandleContextMenu(rect, node, enabled);
@@ -194,6 +197,49 @@ private void OnNewCurveAdded(AddCurvesPopupPropertyNode node)
{
}
+ void CalculateRects(Rect rect, AnimationWindowHierarchyNode node, float indent, out Rect labelRect, out Rect fieldRect)
+ {
+ // Calculate labelRect
+ labelRect = rect;
+
+ // Offset the labelRect with the amount of indent and the foldout width
+ labelRect.xMin += (int)(indent + foldoutStyleWidth + lineStyle.margin.left);
+ labelRect.yMin = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2;
+
+ // labelRect will take the entire row unless the node is a property.
+ // Return early and avoid fieldRect calculations.
+ if (node is not AnimationWindowHierarchyPropertyNode propertyNode)
+ {
+ fieldRect = Rect.zero; // no field on the right
+ return;
+ }
+
+ var offset = propertyNode.isPPtrNode
+ ? k_ObjectFieldOffsetFromRightSide
+ : k_ValueFieldOffsetFromRightSide;
+ var fieldWidth = propertyNode.isPPtrNode
+ ? k_ObjectFieldWidth
+ : k_ValueFieldWidth;
+
+ // Adjust labelRect based on the space taken by fieldRect
+ labelRect.xMax -= offset + k_LabelFieldSpacing;
+
+ // Calculate fieldRect
+ fieldRect = rect;
+ fieldRect.x = rect.xMax - offset;
+ fieldRect.width = fieldWidth;
+
+ // Limit height of PPtr field to avoid showing a squashed preview of the object
+ if (propertyNode.isPPtrNode)
+ {
+ var height = Mathf.Min(k_ObjectFieldMaxHeight, fieldRect.height);
+ var yOffset = (fieldRect.height - height) * 0.5f;
+
+ fieldRect.y += yOffset;
+ fieldRect.height = height;
+ }
+ }
+
private void DoRowBackground(Rect rect, int row)
{
if (Event.current.type != EventType.Repaint)
@@ -244,7 +290,7 @@ private void DoFoldout(AnimationWindowHierarchyNode node, Rect rect, float inden
}
}
- private void DoIconAndName(Rect rect, AnimationWindowHierarchyNode node, bool selected, bool focused, float indent)
+ private void DoIconAndName(Rect rect, Rect labelRect, AnimationWindowHierarchyNode node, bool selected, bool focused)
{
EditorGUIUtility.SetIconSize(new Vector2(13, 13)); // If not set we see icons scaling down if text is being cropped
@@ -254,10 +300,6 @@ private void DoIconAndName(Rect rect, AnimationWindowHierarchyNode node, bool se
if (selected)
selectionStyle.Draw(rect, false, false, true, focused);
- // Leave some space for the value field that comes after.
- if (node is AnimationWindowHierarchyPropertyNode)
- rect.width -= k_ValueFieldOffsetFromRightSide + 2;
-
bool isLeftOverCurve = AnimationWindowUtility.IsNodeLeftOverCurve(state, node);
bool isAmbiguous = AnimationWindowUtility.IsNodeAmbiguous(state, node);
bool isPhantom = AnimationWindowUtility.IsNodePhantom(node);
@@ -323,15 +365,15 @@ private void DoIconAndName(Rect rect, AnimationWindowHierarchyNode node, bool se
SetStyleTextColor(lineStyle, textColor);
- rect.xMin += (int)(indent + foldoutStyleWidth + lineStyle.margin.left);
- rect.yMin = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2;
- GUI.Label(rect, Styles.content, lineStyle);
+ GUI.Label(labelRect, Styles.content, lineStyle);
SetStyleTextColor(lineStyle, oldColor);
+
+ GUIView.current?.MarkHotRegion(GUIClip.UnclipToWindow(labelRect));
}
if (IsRenaming(node.id) && Event.current.type != EventType.Layout)
- GetRenameOverlay().editFieldRect = new Rect(rect.x + k_IndentWidth, rect.y, rect.width - k_IndentWidth - 1, rect.height);
+ GetRenameOverlay().editFieldRect = new Rect(labelRect.x + k_IndentWidth, labelRect.y, labelRect.width - k_IndentWidth - 1, labelRect.height);
}
private string GetGameObjectName(GameObject rootGameObject, string path)
@@ -343,7 +385,7 @@ private string GetGameObjectName(GameObject rootGameObject, string path)
return splits[splits.Length - 1];
}
- private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
+ private void DoValueField(Rect rect, Rect fieldRect, AnimationWindowHierarchyNode node, int row)
{
bool curvesChanged = false;
@@ -359,10 +401,9 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
int id = m_HierarchyItemValueControlIDs[row];
- Rect valueFieldDragRect = new Rect(rect.xMax - k_ValueFieldOffsetFromRightSide - k_ValueFieldDragWidth, rect.y, k_ValueFieldDragWidth, rect.height);
- Rect valueFieldRect = new Rect(rect.xMax - k_ValueFieldOffsetFromRightSide, rect.y, k_ValueFieldWidth, rect.height);
+ Rect valueFieldDragRect = new Rect(fieldRect.x - k_ValueFieldDragWidth, rect.y, k_ValueFieldDragWidth, rect.height);
- if (Event.current.type == EventType.MouseMove && valueFieldRect.Contains(Event.current.mousePosition))
+ if (Event.current.type == EventType.MouseMove && fieldRect.Contains(Event.current.mousePosition))
s_WasInsideValueRectFrame = Time.frameCount;
EditorGUI.BeginChangeCheck();
@@ -370,7 +411,7 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
bool handledByCustomHandler = false;
foreach (var handler in AnimationWindowUtility.PropertyHandlers)
{
- if (handler.TryDoValueField(valueFieldRect, valueFieldDragRect, id,
+ if (handler.TryDoValueField(fieldRect, valueFieldDragRect, id,
curve.binding, curve.valueType, node.animatableObjectType, value,
out var handlerValue, ref node.handlerData))
{
@@ -388,16 +429,12 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
if (typeof(UnityEngine.Object).IsAssignableFrom(objType))
{
- var height = Mathf.Min(k_ObjectFieldMaxHeight, valueFieldRect.height);
- var yOffset = (valueFieldRect.height - height) * 0.5f;
- valueFieldRect = new Rect(valueFieldRect.x - k_ObjectFieldAdditionalOffset, valueFieldRect.y + yOffset, valueFieldRect.width + k_ObjectFieldAdditionalWidth, height);
-
- value = EditorGUI.DoObjectField(valueFieldRect, valueFieldRect, id, value as UnityEngine.Object, null, objType, null, false);
+ value = EditorGUI.DoObjectField(fieldRect, fieldRect, id, value as UnityEngine.Object, null, objType, null, false);
}
}
else if (curve.valueType == typeof(bool))
{
- value = GUI.Toggle(valueFieldRect, id, Convert.ToSingle(value) != 0f, GUIContent.none, EditorStyles.toggle) ? 1f : 0f;
+ value = GUI.Toggle(fieldRect, id, Convert.ToSingle(value) != 0f, GUIContent.none, EditorStyles.toggle) ? 1f : 0f;
}
else
{
@@ -408,7 +445,7 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
// Force back keyboard focus to float field editor when editing it since the TreeView forces keyboard focus on itself at mouse down.
// The focus will be reclaimed after the TreeViewController.OnGUI call.
- if (EditorGUI.s_RecycledEditor.controlID == id && Event.current.type == EventType.MouseDown && valueFieldRect.Contains(Event.current.mousePosition))
+ if (EditorGUI.s_RecycledEditor.controlID == id && Event.current.type == EventType.MouseDown && fieldRect.Contains(Event.current.mousePosition))
{
m_NeedsToReclaimFieldFocus = true;
m_FieldToReclaimFocus = id;
@@ -417,7 +454,7 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
if (curve.isDiscreteCurve)
{
value = EditorGUI.DoIntField(EditorGUI.s_RecycledEditor,
- valueFieldRect,
+ fieldRect,
valueFieldDragRect,
id,
Convert.ToInt32(value),
@@ -434,7 +471,7 @@ private void DoValueField(Rect rect, AnimationWindowHierarchyNode node, int row)
else
{
value = EditorGUI.DoFloatField(EditorGUI.s_RecycledEditor,
- valueFieldRect,
+ fieldRect,
valueFieldDragRect,
id,
Convert.ToSingle(value),
@@ -494,6 +531,7 @@ private bool DoTreeViewButton(int id, Rect position, GUIContent content, GUIStyl
{
case EventType.Repaint:
style.Draw(position, content, id, false, position.Contains(evt.mousePosition));
+ GUIView.current?.MarkHotRegion(GUIClip.UnclipToWindow(position));
break;
case EventType.MouseDown:
if (position.Contains(evt.mousePosition) && evt.button == 0)
@@ -749,7 +787,6 @@ void ChangeRotationInterpolation(System.Object interpolationMode)
}
state.activeClip.SetInterpolations(curveBindings, mode, L10n.Tr("Rotation Interpolation"));
- MaintainTreeviewStateAfterRotationInterpolation(mode);
state.hierarchyData.ReloadData();
}
@@ -824,54 +861,6 @@ private List GetCurvesAffectedByNodes(List selectedInstaceIDs = state.hierarchyState.selectedIDs;
- List expandedInstaceIDs = state.hierarchyState.expandedIDs;
-
- List oldIDs = new List();
- List newIds = new List();
-
- for (int i = 0; i < selectedInstaceIDs.Count; i++)
- {
- AnimationWindowHierarchyNode node = state.hierarchyData.FindItem(selectedInstaceIDs[i]) as AnimationWindowHierarchyNode;
-
- if (node != null && !node.propertyName.Equals(RotationCurveInterpolation.GetPrefixForInterpolation(newMode)))
- {
- string oldPrefix = node.propertyName.Split('.')[0];
- string newPropertyName = node.propertyName.Replace(oldPrefix, RotationCurveInterpolation.GetPrefixForInterpolation(newMode));
-
- // old treeview node id
- oldIDs.Add(selectedInstaceIDs[i]);
- // and its new replacement
- newIds.Add((node.path + node.animatableObjectType.Name + newPropertyName).GetHashCode());
- }
- }
-
- // Replace old IDs with new ones
- for (int i = 0; i < oldIDs.Count; i++)
- {
- if (selectedInstaceIDs.Contains(oldIDs[i]))
- {
- int index = selectedInstaceIDs.IndexOf(oldIDs[i]);
- selectedInstaceIDs[index] = newIds[i];
- }
- if (expandedInstaceIDs.Contains(oldIDs[i]))
- {
- int index = expandedInstaceIDs.IndexOf(oldIDs[i]);
- expandedInstaceIDs[index] = newIds[i];
- }
- if (state.hierarchyState.lastClickedID == oldIDs[i])
- state.hierarchyState.lastClickedID = newIds[i];
- }
-
- state.hierarchyState.selectedIDs = new List(selectedInstaceIDs);
- state.hierarchyState.expandedIDs = new List(expandedInstaceIDs);
- }
-
private RotationCurveInterpolation.Mode GetRotationInterpolationMode(EditorCurveBinding[] curves)
{
if (curves == null || curves.Length == 0)
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowSearchFilter.cs b/Modules/AnimationWindow/Editor/AnimationWindowSearchFilter.cs
index a0fb8d8b0f..615945e318 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowSearchFilter.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowSearchFilter.cs
@@ -13,6 +13,7 @@ struct AnimationWindowSearchFilter
{
static readonly string[] k_TypePrefixes = ["t=", "type="];
static readonly string[] k_PropertyPrefixes = ["p=", "property="];
+ public const string k_FilterPrefix = "animation:";
static readonly char[] kFilterSeparator = new [] { ' ', '\t', ',', '*', '?'};
[SerializeField] List m_NameFilters = new();
@@ -102,6 +103,12 @@ void SetSearchString(string searchString)
void CheckForKeyWords(string searchString, int quote1, int quote2)
{
+ // Ignore filter prefix
+ if (searchString.StartsWith(k_FilterPrefix))
+ {
+ searchString = searchString.Substring(k_FilterPrefix.Length);
+ }
+
// Support: 't=type' syntax (e.g 't=Transform' will show Transform components)
foreach (var typePrefix in k_TypePrefixes)
{
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowSearchView.cs b/Modules/AnimationWindow/Editor/AnimationWindowSearchView.cs
index e3d6c23016..8e1fb95e9b 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowSearchView.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowSearchView.cs
@@ -40,7 +40,8 @@ SearchProvider CreateAnimationProvider()
{
return new SearchProvider("animation", "Animation")
{
- isExplicitProvider = false,
+ isExplicitProvider = true,
+ filterId = AnimationWindowSearchFilter.k_FilterPrefix,
priority = 100,
active = true,
fetchPropositions = (context, options) => FetchAnimationPropositions(context, options)
@@ -151,7 +152,7 @@ void ISearchView.SetSearchText(string searchText, TextCursorPlacement moveCursor
return;
context.searchText = searchText;
- m_State.searchFilter = context.searchQuery;
+ m_State.searchFilter = searchText;
}
void ISearchView.SetSelection(params int[] selection)
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowState.cs b/Modules/AnimationWindow/Editor/AnimationWindowState.cs
index c489001268..31c3c5379c 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowState.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowState.cs
@@ -38,6 +38,7 @@ public enum SnapMode
[SerializeField] public AnimEditor animEditor; // Reference to owner of this state. Used to trigger repaints.
[SerializeField] public AnimationWindowHierarchyState hierarchyState = new(); // Persistent state of treeview on the left side of window
[NonSerialized] public AnimationWindowHierarchyDataSource hierarchyData;
+ [NonSerialized] public Action onHierarchySelectionChange;
[SerializeReference] private TimeArea m_TimeArea; // Either curveeditor or dopesheet depending on which is selected
@@ -1565,7 +1566,7 @@ public void HandleHierarchySelectionChanged(int[] selectedInstanceIDs, bool trig
m_ActiveCurvesCache = null;
if (triggerSceneSelectionSync)
- SyncSceneSelection(selectedInstanceIDs);
+ onHierarchySelectionChange?.Invoke(selectedInstanceIDs);
}
public void SelectHierarchyItem(DopeLine dopeline, bool additive)
@@ -1667,65 +1668,6 @@ public DopeLine GetDopeline(int selectedInstanceID)
return null;
}
- // Set scene active go to be the same as the one selected from hierarchy
- private void SyncSceneSelection(int[] selectedNodeIDs)
- {
- if (filterBySelection)
- return;
-
- if (!selection.canSyncSceneSelection)
- return;
-
- GameObject rootGameObject = selection.rootGameObject;
- if (rootGameObject == null)
- return;
-
- var selectedGameObjectIDs = new List(selectedNodeIDs.Length);
- foreach (var selectedNodeID in selectedNodeIDs)
- {
- // Skip nodes without associated curves.
- if (selectedNodeID == 0)
- continue;
-
- AnimationWindowHierarchyNode node = hierarchyData.FindItem(selectedNodeID) as AnimationWindowHierarchyNode;
-
- if (node == null)
- continue;
-
- if (node is AnimationWindowHierarchyMasterNode)
- continue;
-
- Transform t = rootGameObject.transform.Find(node.path);
-
- // In the case of nested animation component, we don't want to sync the scene selection (case 569506)
- // When selection changes, animation window will always pick nearest animator component in terms of hierarchy depth
- // Automatically syncinc scene selection in nested scenarios would cause unintuitive clip & animation change for animation window so we check for it and deny sync if necessary
- if (selection.IsCompatibleWith(t))
- {
- EntityId entity;
- if (node.curves.Length > 0)
- {
- AnimationWindowCurve firstCurve = node.curves[0];
- // Query the animation system for the associate EntityId
- // For custom IAnimationBinding (e.g., UIToolkit), this will return the appropriate selection object
- // For standard animations, returns the GameObject's EntityId
- entity = AnimationUtility.GetAssociatedEntityId(t.gameObject, firstCurve.binding);
- }
- else
- {
- entity = t.gameObject.GetEntityId();
- }
-
- selectedGameObjectIDs.Add(entity);
- }
- }
-
- if (selectedGameObjectIDs.Count > 0)
- UnityEditor.Selection.entityIds = selectedGameObjectIDs.ToArray();
- else
- UnityEditor.Selection.activeGameObject = rootGameObject;
- }
-
[Obsolete("Use frameRate property instead.")]
public float clipFrameRate
{
@@ -1776,6 +1718,11 @@ public float frameRate
}
selection.clip.frameRate = value;
+
+ // Preserve the current frame position at the new frame rate
+ int oldFrame = AnimationKeyTime.Time(currentTime, oldFrameRate).frame;
+ float newTime = AnimationKeyTime.Frame(oldFrame, value).time;
+ currentTime = newTime;
}
}
}
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowStyles.cs b/Modules/AnimationWindow/Editor/AnimationWindowStyles.cs
index c5c29fadd3..b68190e0da 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowStyles.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowStyles.cs
@@ -68,6 +68,9 @@ internal class AnimationWindowStyles
public static GUIStyle miniToolbarButton = new GUIStyle(EditorStyles.toolbarButton);
public static GUIStyle toolbarLabel = new GUIStyle(AnimationWindowStyles.animClipToolbarPopup);
+ public static readonly GUIStyle plusButton = "IconButton";
+ public static readonly GUIStyle plusButtonBackground = "Tag MenuItem";
+
public static void Initialize()
{
toolbarLabel.normal.background = null;
diff --git a/Modules/AnimationWindow/Editor/AnimationWindowUtility.cs b/Modules/AnimationWindow/Editor/AnimationWindowUtility.cs
index 45d8e02b14..bbcd3c3b43 100644
--- a/Modules/AnimationWindow/Editor/AnimationWindowUtility.cs
+++ b/Modules/AnimationWindow/Editor/AnimationWindowUtility.cs
@@ -16,6 +16,8 @@ namespace UnityEditorInternal
[VisibleToOtherModules("UnityEditor.UIToolkitAuthoringModule")]
static partial class AnimationWindowUtility
{
+ public const float MaxDisplayableKeyValue = 5e5f;
+
private static readonly List s_PropertyHandlers = new();
[VisibleToOtherModules("UnityEditor.UIToolkitAuthoringModule")]
@@ -51,6 +53,10 @@ static void SetDefaultValues(AnimationWindowState state, IAnimationWindowClip cl
Type type = state.selection.GetValueType(curve.binding);
object currentValue = CurveBindingUtility.GetCurrentValue(state, curve.binding);
+
+ if (currentValue is float f)
+ currentValue = float.IsNaN(f) ? 0f : Mathf.Clamp(f, -MaxDisplayableKeyValue, MaxDisplayableKeyValue);
+
if (curve.length == 0.0F)
{
AddKeyframeToCurve(curve, currentValue, type, AnimationKeyTime.Time(0.0F, clip.frameRate));
@@ -561,9 +567,12 @@ public static bool CurveExists(EditorCurveBinding binding, AnimationWindowCurve[
private const string k_ComponentPathSeparator = " : ";
- private static string FormatComponentPathDisplayName(Type componentType, string displayPath)
+ private static string FormatComponentPathDisplayName(Type componentType, string propertyName, string displayPath)
{
- return ObjectNames.NicifyVariableName(componentType.Name) + k_ComponentPathSeparator + displayPath.Replace("/", k_ComponentPathSeparator);
+ string path = displayPath.Replace("/", k_ComponentPathSeparator);
+ if (!ShouldPrefixWithTypeName(componentType, propertyName))
+ return path;
+ return ObjectNames.NicifyVariableName(componentType.Name) + k_ComponentPathSeparator + path;
}
// Takes raw animation curve propertyname and makes it pretty
@@ -594,6 +603,12 @@ public static bool ShouldPrefixWithTypeName(Type animatableObjectType, string pr
if (animatableObjectType == typeof(SpriteRenderer) && propertyName == "m_Sprite")
return false;
+ // A registered handler may declare this curve component-less (e.g. binder-routed style
+ // channels with no owning component on a GameObject), in which case it gets no prefix.
+ foreach (var h in s_PropertyHandlers)
+ if (!h.ShouldPrefixWithTypeName(animatableObjectType, propertyName))
+ return false;
+
return true;
}
@@ -624,7 +639,7 @@ public static string GetNicePropertyDisplayName(EditorCurveBinding curveBinding,
if (!string.IsNullOrEmpty(curveBinding.propertyName) && curveBinding.propertyName.IndexOf('/') >= 0)
{
GetGroupDisplayPath(curveBinding.propertyName, out string displayPath, out fullPathForTooltip);
- return FormatComponentPathDisplayName(animatableObjectType, displayPath);
+ return FormatComponentPathDisplayName(animatableObjectType, curveBinding.propertyName, displayPath);
}
fullPathForTooltip = null;
return GetNicePropertyDisplayName(curveBinding, so);
@@ -665,7 +680,7 @@ public static string GetNicePropertyGroupDisplayName(EditorCurveBinding curveBin
if (!string.IsNullOrEmpty(curveBinding.propertyName) && curveBinding.propertyName.IndexOf('/') >= 0)
{
GetGroupDisplayPath(curveBinding.propertyName, out string displayPath, out fullPathForTooltip);
- return FormatComponentPathDisplayName(animatableObjectType, displayPath);
+ return FormatComponentPathDisplayName(animatableObjectType, curveBinding.propertyName, displayPath);
}
fullPathForTooltip = null;
return GetNicePropertyGroupDisplayName(curveBinding, so);
@@ -875,7 +890,7 @@ public static float GetPreviousKeyframeTime(IEnumerable cu
public static int GetPropertyNodeID(int setId, string path, System.Type type, string propertyName)
{
- return (setId + path + type.Name + propertyName).GetHashCode();
+ return (setId + path + type.Name + RotationCurveInterpolation.GetPropertyNameForHashing(type, propertyName)).GetHashCode();
}
public static void SyncTimeArea(TimeArea from, TimeArea to)
diff --git a/Modules/AnimationWindow/Editor/DopeSheetEditor.cs b/Modules/AnimationWindow/Editor/DopeSheetEditor.cs
index 8a603261a9..efc9dce826 100644
--- a/Modules/AnimationWindow/Editor/DopeSheetEditor.cs
+++ b/Modules/AnimationWindow/Editor/DopeSheetEditor.cs
@@ -890,11 +890,14 @@ public EditorCurveBinding[] GetAnimatableProperties(IAnimationWindowSelectionIte
private void SelectTypeForCreatingNewPptrDopeline(object userData, string[] options, int selected)
{
+ if (selected < 0)
+ return;
+
List userDataList = userData as List;
var clip = userDataList[0] as IAnimationWindowClip;
- List bindings = userDataList[1] as List;
+ var bindings = userDataList[1] as EditorCurveBinding[];
- if (bindings.Count > selected)
+ if (bindings.Length > selected)
DoSpriteDropAfterGeneratingNewDopeline(clip, bindings[selected]);
}
@@ -1384,7 +1387,7 @@ internal class DopeSheetSelectionRect
enum SelectionType { Normal, Additive, Subtractive }
public readonly GUIStyle createRect = "U2D.createRect";
- static int s_RectSelectionID = GUIUtility.GetPermanentControlID();
+ readonly int m_RectSelectionID = GUIUtility.GetPermanentControlID();
public DopeSheetSelectionRect(DopeSheetEditor owner)
{
@@ -1395,7 +1398,7 @@ public void OnGUI(Rect position)
{
Event evt = Event.current;
Vector2 mousePos = evt.mousePosition;
- int id = s_RectSelectionID;
+ int id = m_RectSelectionID;
switch (evt.GetTypeForControl(id))
{
case EventType.MouseDown:
diff --git a/Modules/AnimationWindow/Editor/IAnimationWindowPropertyHandler.cs b/Modules/AnimationWindow/Editor/IAnimationWindowPropertyHandler.cs
index 5cdb99f56b..dab6eb5548 100644
--- a/Modules/AnimationWindow/Editor/IAnimationWindowPropertyHandler.cs
+++ b/Modules/AnimationWindow/Editor/IAnimationWindowPropertyHandler.cs
@@ -18,6 +18,11 @@ internal interface IAnimationWindowPropertyHandler
// Returns the group name (prefix before the channel suffix), or null if not handled.
string GetPropertyGroupName(string propertyName);
+ // Returns false when this curve has no owning component to name so the Animation Window omits
+ // the component-name prefix from its hierarchy row.
+ // Return true to keep the default behaviour of prefixing with the owning component's type name.
+ bool ShouldPrefixWithTypeName(Type animatableObjectType, string propertyName);
+
// --- Value field rendering (called inside BeginChangeCheck/EndChangeCheck) ---
// Renders a custom value control for the given curve.
// animatableObjectType is the node's component type; handlers should
diff --git a/Modules/AnimationWindow/Editor/RotationCurveInterpolation.cs b/Modules/AnimationWindow/Editor/RotationCurveInterpolation.cs
index 26f6527822..ae2fb9dfe6 100644
--- a/Modules/AnimationWindow/Editor/RotationCurveInterpolation.cs
+++ b/Modules/AnimationWindow/Editor/RotationCurveInterpolation.cs
@@ -44,6 +44,31 @@ public static string GetPrefixForInterpolation(Mode newInterpolationMode)
return null;
}
+ // Maps all rotation euler interpolation variants to their m_LocalRotation equivalent
+ // so that node IDs stay stable when the interpolation mode changes.
+ private static readonly Dictionary s_PropertyNameForHashing = new Dictionary(StringComparer.Ordinal)
+ {
+ { "localEulerAnglesRaw", "m_LocalRotation" },
+ { "localEulerAnglesRaw.x", "m_LocalRotation.x" },
+ { "localEulerAnglesRaw.y", "m_LocalRotation.y" },
+ { "localEulerAnglesRaw.z", "m_LocalRotation.z" },
+ { "localEulerAnglesBaked", "m_LocalRotation" },
+ { "localEulerAnglesBaked.x", "m_LocalRotation.x" },
+ { "localEulerAnglesBaked.y", "m_LocalRotation.y" },
+ { "localEulerAnglesBaked.z", "m_LocalRotation.z" },
+ { "localEulerAngles", "m_LocalRotation" },
+ { "localEulerAngles.x", "m_LocalRotation.x" },
+ { "localEulerAngles.y", "m_LocalRotation.y" },
+ { "localEulerAngles.z", "m_LocalRotation.z" },
+ };
+
+ internal static string GetPropertyNameForHashing(System.Type type, string propertyName)
+ {
+ if (type != typeof(UnityEngine.Transform) || !propertyName.StartsWith("localEuler"))
+ return propertyName;
+ return s_PropertyNameForHashing.TryGetValue(propertyName, out string canonical) ? canonical : propertyName;
+ }
+
static List s_BindingsCache;
const string s_PropertyWithSuffixRegex = @"(?\.[xyz])$";
internal static EditorCurveBinding[] ConvertRotationPropertiesToInterpolationType(ReadOnlySpan selection, Mode newInterpolationMode)
diff --git a/Modules/AnimationWindow/Editor/Widgets/PlayControls.cs b/Modules/AnimationWindow/Editor/Widgets/PlayControls.cs
index c75317d93a..c615d2723a 100644
--- a/Modules/AnimationWindow/Editor/Widgets/PlayControls.cs
+++ b/Modules/AnimationWindow/Editor/Widgets/PlayControls.cs
@@ -71,6 +71,9 @@ public void Dispose()
public void Update()
{
+ if (playToggle.enabledSelf != m_State.canPlay)
+ playToggle.SetEnabled(m_State.canPlay);
+
if (playToggle.value != m_State.playing)
playToggle.SetValueWithoutNotify(m_State.playing);
diff --git a/Modules/AssetBundle/Managed/AssetBundle.bindings.cs b/Modules/AssetBundle/Managed/AssetBundle.bindings.cs
index 92e3696344..858acbec5a 100644
--- a/Modules/AssetBundle/Managed/AssetBundle.bindings.cs
+++ b/Modules/AssetBundle/Managed/AssetBundle.bindings.cs
@@ -35,7 +35,7 @@ public enum AssetBundleLoadResult
[NativeHeader("Modules/AssetBundle/Public/AssetBundleLoadFromManagedStreamAsyncOperation.h")]
[NativeHeader("Modules/AssetBundle/Public/AssetBundleLoadAssetOperation.h")]
[NativeHeader("Runtime/Scripting/ScriptingExportUtility.h")]
- [NativeHeader("Runtime/Scripting/ScriptingUtility.h")]
+ [NativeHeader("Scripting/ScriptingUtility.h")]
[NativeHeader("AssetBundleScriptingClasses.h")]
[NativeHeader("Modules/AssetBundle/Public/AssetBundleSaveAndLoadHelper.h")]
[NativeHeader("Modules/AssetBundle/Public/AssetBundleUtility.h")]
diff --git a/Modules/AssetDatabase/Editor/V2/Managed/ImportActivityWindow.cs b/Modules/AssetDatabase/Editor/V2/Managed/ImportActivityWindow.cs
index 55f51776e0..e91e2c1b72 100644
--- a/Modules/AssetDatabase/Editor/V2/Managed/ImportActivityWindow.cs
+++ b/Modules/AssetDatabase/Editor/V2/Managed/ImportActivityWindow.cs
@@ -904,7 +904,10 @@ private void RevealSelectedArtifactInfinder()
const string kUDSTempFolder = "Temp/UDS";
Directory.CreateDirectory(kUDSTempFolder);
var tempPath = Path.Combine(kUDSTempFolder, Path.GetFileName(entry.libraryPath));
- FileUtil.CopyFileOrDirectory(entry.libraryPath, tempPath);
+
+ if (!File.Exists(tempPath))
+ FileUtil.CopyFileOrDirectory(entry.libraryPath, tempPath);
+
EditorUtility.RevealInFinder(tempPath);
}
else
diff --git a/Modules/AssetPackageUIEditor/Managed/PackageImport.cs b/Modules/AssetPackageUIEditor/Managed/PackageImport.cs
index 82d513f8bd..5aef221101 100644
--- a/Modules/AssetPackageUIEditor/Managed/PackageImport.cs
+++ b/Modules/AssetPackageUIEditor/Managed/PackageImport.cs
@@ -68,7 +68,7 @@ public Constants()
[NativeHeader("Modules/AssetPackageEditor/AssetPackage.bindings.h")]
[FreeFunction("Marshalling::GetAssetPackageInfo")]
- private static extern AssetPackageInfo GetAssetPackageInfo(IntPtr nativeAssetPackageInfo);
+ internal static extern AssetPackageInfo GetAssetPackageInfo(IntPtr nativeAssetPackageInfo);
// Invoked from menu
[UsedByNativeCode]
diff --git a/Modules/AssetPipelineEditor/ImportSettings/AssetImporterEditor.cs b/Modules/AssetPipelineEditor/ImportSettings/AssetImporterEditor.cs
index 922897d138..355bf63ae3 100644
--- a/Modules/AssetPipelineEditor/ImportSettings/AssetImporterEditor.cs
+++ b/Modules/AssetPipelineEditor/ImportSettings/AssetImporterEditor.cs
@@ -379,6 +379,23 @@ public virtual void OnEnable()
finishedDefaultHeaderGUI += DrawAssetHasIssuesNotification;
AssetImporterEditorPostProcessAsset.OnAssetbundleNameChanged += FixImporterAssetbundleName;
+ // A target importer's native object can be destroyed while this managed editor is still
+ // alive: the asset was deleted or its .meta guid was rewritten on disk, then a domain
+ // reload re-awakes this editor from backup (MonoBehaviour.DidReloadDomain -> OnEnable).
+ // Target-dependent initialization below would either dereference a dangling EntityId
+ // (native crash in CreateOrReloadInspectorCopy/WriteObjectToVector) or throw
+ // (GetAssetPaths().First() on an empty sequence). A destroyed target is a valid, inert
+ // state for an editor: subscribe/unsubscribe symmetrically, mark it enabled, and stop.
+ if (!AreImporterTargetsValid())
+ {
+ Debug.Log("AssetImporterEditor: one or more inspected importer targets no longer exist (asset deleted or its GUID changed); skipping inspector setup.");
+ m_Postprocessors = new List();
+ m_TargetsEntityId = new List();
+ m_OnEnableCalled = true;
+ isInspectorDirty = true;
+ return;
+ }
+
InitializeAvailableImporters();
InitializeUnsavedChangesCache();
InitializePostprocessors();
@@ -395,6 +412,22 @@ public virtual void OnEnable()
isInspectorDirty = true;
}
+ // True only when every target importer's native object still exists. A target can be
+ // destroyed (asset deleted, or its .meta GUID rewritten on disk) while this managed editor
+ // is still alive and later re-enabled by a domain reload. Subclasses must call this before
+ // accessing serializedObject or other target-dependent state; otherwise serializedObject
+ // throws SerializedObjectNotCreatableException and native code dereferences a stale EntityId.
+ protected bool AreImporterTargetsValid()
+ {
+ foreach (var t in targets)
+ {
+ var importer = t as AssetImporter;
+ if (importer == null) // UnityEngine.Object overloaded ==: true for null or destroyed
+ return false;
+ }
+ return true;
+ }
+
public virtual void OnDisable()
{
finishedDefaultHeaderGUI -= DrawAssetHasIssuesNotification;
diff --git a/Modules/AssetPipelineEditor/ImportSettings/AssetImporterTabbedEditor.cs b/Modules/AssetPipelineEditor/ImportSettings/AssetImporterTabbedEditor.cs
index 0f374e6005..ac7ce2a10e 100644
--- a/Modules/AssetPipelineEditor/ImportSettings/AssetImporterTabbedEditor.cs
+++ b/Modules/AssetPipelineEditor/ImportSettings/AssetImporterTabbedEditor.cs
@@ -24,6 +24,8 @@ internal abstract class AssetImporterTabbedEditor : AssetImporterEditor
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed; do not enable tabs on a dead target
+ return;
foreach (var tab in m_Tabs)
{
diff --git a/Modules/AssetPipelineEditor/ImportSettings/AudioImporterInspector.cs b/Modules/AssetPipelineEditor/ImportSettings/AudioImporterInspector.cs
index 7876aa20bf..7aa441e053 100644
--- a/Modules/AssetPipelineEditor/ImportSettings/AudioImporterInspector.cs
+++ b/Modules/AssetPipelineEditor/ImportSettings/AudioImporterInspector.cs
@@ -187,6 +187,8 @@ public bool CurrentSelectionContainsHardwareSounds()
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
m_ForceToMono = serializedObject.FindProperty("m_ForceToMono");
m_Normalize = serializedObject.FindProperty("m_Normalize");
diff --git a/Modules/AssetPipelineEditor/ImportSettings/PluginImporterInspector.cs b/Modules/AssetPipelineEditor/ImportSettings/PluginImporterInspector.cs
index 2ebbfc2677..8c364a8846 100644
--- a/Modules/AssetPipelineEditor/ImportSettings/PluginImporterInspector.cs
+++ b/Modules/AssetPipelineEditor/ImportSettings/PluginImporterInspector.cs
@@ -491,6 +491,8 @@ protected override void Awake()
public override void OnEnable()
{
base.OnEnable();
+ if (!AreImporterTargetsValid()) // asset gone: base already logged and bailed
+ return;
// this method is doing a lot of setup and it used to be called in awake for some old reasons, which is not the case anymore.
DiscardChanges();
diff --git a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ControlContext.cs b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ControlContext.cs
index fd6313de26..6ce4271957 100644
--- a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ControlContext.cs
+++ b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ControlContext.cs
@@ -229,30 +229,39 @@ public readonly GeneratorInstance AllocateGenerator(
{
m_Handle.CheckValidOrThrow();
- var generatorChunk = ProcessorExtensions.CAllocChunk.ControlStorage>();
-
- var header = &generatorChunk->HeaderAndProcessor.Header;
+ DualThreadHandle createdHandle;
+ {
+ // Stage the ControlStorage on the stack. Native memcpys it into the bridge slab tail; this local
+ // dies as soon as InitializeGeneratorHandle returns.
+ IGeneratorControlExtensions.JobStruct.ControlStorage storage;
- header->Processor.ProcessorReflectionData = IGeneratorProcessorExtensions.GetReflectionData();
- header->Processor.ControlReflectionData = IGeneratorControlExtensions.GetReflectionData();
+ storage.HeaderAndProcessor.Header = default;
+ storage.HeaderAndProcessor.Header.Processor.ProcessorReflectionData = IGeneratorProcessorExtensions.GetReflectionData();
+ storage.HeaderAndProcessor.Header.Processor.ControlReflectionData = IGeneratorControlExtensions.GetReflectionData();
- header->Configuration.IsRealtime = realtimeState.isRealtime;
- header->Configuration.IsFinite = realtimeState.isFinite;
+ storage.HeaderAndProcessor.Header.Configuration.IsRealtime = realtimeState.isRealtime;
+ storage.HeaderAndProcessor.Header.Configuration.IsFinite = realtimeState.isFinite;
- if (realtimeState.length is DiscreteTime time)
- {
- header->Configuration.ReportedLength = time;
- header->Configuration.HasKnownLength = true;
- }
+ if (realtimeState.length is DiscreteTime time)
+ {
+ storage.HeaderAndProcessor.Header.Configuration.ReportedLength = time;
+ storage.HeaderAndProcessor.Header.Configuration.HasKnownLength = true;
+ }
- generatorChunk->HeaderAndProcessor.UserProcessor = realtimeState;
- generatorChunk->UserControl = controlState;
+ storage.HeaderAndProcessor.UserProcessor = realtimeState;
+ storage.UserControl = controlState;
- var config = (nestedFormat ?? default).audioConfiguration;
+ var config = (nestedFormat ?? default).audioConfiguration;
- ScriptableGeneratorBindings.InitializeGeneratorHandle(header, m_Header, nestedFormat.HasValue ? &config : null, creationParameters.BuildInitializationFlags());
+ createdHandle = ScriptableGeneratorBindings.InitializeGeneratorHandle(
+ ref storage,
+ m_Header,
+ nestedFormat.HasValue ? &config : null,
+ creationParameters.BuildInitializationFlags()
+ );
+ }
- return new(header);
+ return new GeneratorInstance(GetProcessorHeader(createdHandle));
}
///
@@ -275,20 +284,28 @@ public readonly RootOutputInstance AllocateRootOutput(in TR
where TControl : unmanaged, RootOutputInstance.IControl
{
m_Handle.CheckValidOrThrow();
+ DualThreadHandle createdHandle;
- var outputChunk = ProcessorExtensions.CAllocChunk.ControlStorage>();
-
- var header = &outputChunk->HeaderAndProcessor.Header;
-
- header->ProcessorReflectionData = IRootOutputProcessorExtensions.GetReflectionData();
- header->ControlReflectionData = IRootOutputControlExtensions.GetReflectionData();
-
- outputChunk->HeaderAndProcessor.UserProcessor = realtimeState;
- outputChunk->UserControl = controlState;
-
- IRootOutputProcessorExtensions.InitializeRootOutputHandle(header, m_Header, creationParameters.BuildInitializationFlags());
+ {
+ // Stage the ControlStorage on the stack. Native memcpys it into the bridge slab tail; this local
+ // dies as soon as InitializeRootOutputHandle returns.
+ IRootOutputControlExtensions.JobStruct.ControlStorage storage;
+
+ storage.HeaderAndProcessor.Header = default;
+ storage.HeaderAndProcessor.Header.ProcessorReflectionData = IRootOutputProcessorExtensions.GetReflectionData();
+ storage.HeaderAndProcessor.Header.ControlReflectionData = IRootOutputControlExtensions.GetReflectionData();
+
+ storage.HeaderAndProcessor.UserProcessor = realtimeState;
+ storage.UserControl = controlState;
+
+ createdHandle = IRootOutputProcessorExtensions.InitializeRootOutputHandle(
+ ref storage,
+ m_Header,
+ creationParameters.BuildInitializationFlags()
+ );
+ }
- return new(header);
+ return new RootOutputInstance(GetProcessorHeader(createdHandle));
}
///
@@ -567,6 +584,18 @@ internal static unsafe void CleanupHeader(ref ControlHeader header)
}
}
+ ///
+ /// Transition API until a call is made on whether "headers" should be persistently available in "handle wrappers" or not.
+ /// Alternative being resolving on demand, making sure the handle is actually alive and available, at the expense of an ICall every time.
+ /// This is currently necessary to avoid a large number of changes across the codebase, but will likely be removed in the future.
+ ///
+ unsafe readonly THeader* GetProcessorHeader(DualThreadHandle handle)
+ where THeader : unmanaged
+ {
+ m_Handle.CheckValidOrThrow();
+ return (THeader*)ScriptableProcessorBindings.GetProcessorHeaderFromHandle(m_Header, handle);
+ }
+
[NativeMethod(Name = "audio::GetBuiltInControlHeader", IsFreeFunction = true)]
static extern unsafe internal void* InternalGetBuiltInControlHeader();
diff --git a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableGenerator.bindings.cs b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableGenerator.bindings.cs
index 740af3f8d0..f72022c470 100644
--- a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableGenerator.bindings.cs
+++ b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableGenerator.bindings.cs
@@ -714,10 +714,20 @@ internal static unsafe void InstantiateGeneratorFromObject(Object generatorObjec
}
}
- internal static unsafe void InitializeGeneratorHandle(GeneratorInstance.GeneratorHeader* header, ControlHeader* control, AudioConfiguration* nestedConfiguration, ProcessorInstance.InitializationFlags flags)
- => InternalInitializeGeneratorHandle(header, control, nestedConfiguration, flags);
+ internal static unsafe DualThreadHandle InitializeGeneratorHandle(
+ ref IGeneratorControlExtensions.JobStruct.ControlStorage storage,
+ ControlHeader* control,
+ AudioConfiguration* nestedConfiguration,
+ ProcessorInstance.InitializationFlags flags
+ )
+ where TRealtime : unmanaged, GeneratorInstance.IRealtime
+ where TControl : unmanaged, GeneratorInstance.IControl
+ {
+ fixed (GeneratorInstance.GeneratorHeader* headerPtr = &storage.HeaderAndProcessor.Header)
+ return InternalInitializeGeneratorHandle(headerPtr, sizeof(IGeneratorControlExtensions.JobStruct.ControlStorage), control, nestedConfiguration, flags);
+ }
[NativeMethod(Name = "audio::InitializeGeneratorHandle", IsFreeFunction = true, ThrowsException = true)]
- static extern unsafe void InternalInitializeGeneratorHandle(/*Generator.GeneratorHeader* */void* header, /*ControlHeader*/ void* control, AudioConfiguration* nestedConfiguration, ProcessorInstance.InitializationFlags flags);
+ static extern unsafe DualThreadHandle InternalInitializeGeneratorHandle(/*Generator.GeneratorHeader* */void* header, int tailSize, /*ControlHeader*/ void* control, AudioConfiguration* nestedConfiguration, ProcessorInstance.InitializationFlags flags);
}
}
diff --git a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableProcessor.bindings.cs b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableProcessor.bindings.cs
index 0b8e06b426..3b7cd6b712 100644
--- a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableProcessor.bindings.cs
+++ b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableProcessor.bindings.cs
@@ -751,6 +751,12 @@ public static unsafe ProcessorInstance.Response SendMessageToProcessor(Processor
return SendMessageToProcessorInternal(header, control, message);
}
+ public static unsafe ProcessorHeader* GetProcessorHeaderFromHandle(ControlHeader* control, DualThreadHandle handle)
+ {
+ return (ProcessorHeader*)GetProcessorHeaderFromHandleInternal(control, handle);
+ }
+ [NativeMethod(Name = "audio::GetProcessorHeaderFromHandle", IsFreeFunction = true)]
+ static extern unsafe /*ProcessorHeader*/ void* GetProcessorHeaderFromHandleInternal(/*ControlHeader*/ void* control, DualThreadHandle handle);
[NativeMethod(Name = "audio::SendMessageToProcessor", IsFreeFunction = true, ThrowsException = true)]
static extern unsafe ProcessorInstance.Response SendMessageToProcessorInternal(/*ProcessorHeader* */ void* header, /*ControlHeader* */ void* control, /* Message* */ void* message);
@@ -791,14 +797,6 @@ public static unsafe ProcessorInstance.Response SendMessageToProcessor(Processor
static class ProcessorExtensions
{
- internal static unsafe T* CAllocChunk()
- where T : unmanaged
- {
- var chunk = (T*)UnsafeUtility.MallocTracked(sizeof(T), UnsafeUtility.AlignOf(), Allocator.Persistent, 3);
- *chunk = default;
- return chunk;
- }
-
public static unsafe void DispatchGenericControl(ref TControl control, ref TRealtime realtime, in ProcessorHeader header, void* additionalPtr, ControlFunction function)
where TControl : unmanaged, ProcessorInstance.IControl
where TRealtime : unmanaged, ProcessorInstance.IRealtime
@@ -809,10 +807,8 @@ public static unsafe void DispatchGenericControl(ref TContr
{
var args = (DisposeArguments*)additionalPtr;
control.Dispose(new(args->ControlContext), ref realtime);
-
- fixed (ProcessorHeader* pHeader = &header)
- UnsafeUtility.FreeTracked(pHeader, Allocator.Persistent);
-
+ // No header free here: the header lives inside the bridge's native-owned slab, which
+ // the DTM frees with UNITY_FREE once this dispatch returns up through the destructor.
break;
}
case ControlFunction.Update:
diff --git a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableRootOutput.bindings.cs b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableRootOutput.bindings.cs
index 9c67cfff8c..044f8711c1 100644
--- a/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableRootOutput.bindings.cs
+++ b/Modules/Audio/Public/ScriptableProcessors/ScriptBindings/ScriptableRootOutput.bindings.cs
@@ -271,7 +271,7 @@ internal struct Storage
internal static unsafe void Initialize()
{
if (jobReflectionData.Data == IntPtr.Zero)
- jobReflectionData.Data = JobsUtility.CreateJobReflectionData(typeof(Storage), (ExecuteJobFunction)Execute);
+ jobReflectionData.Data = JobsUtility.CreateJobReflectionData(typeof(Storage), typeof(TUserProcessor), (ExecuteJobFunction)Execute);
}
internal delegate void ExecuteJobFunction(ref Storage storage, IntPtr additionalPtr, IntPtr additionalPtr2, ref JobRanges ranges, int jobIndex);
@@ -330,12 +330,22 @@ internal static IntPtr GetReflectionData()
return reflectionData;
}
- internal static unsafe void InitializeRootOutputHandle(ProcessorHeader* header, ControlHeader* control, ProcessorInstance.InitializationFlags flags)
- => InternalInitializeRootOutputHandle(header, control, flags);
+ internal static unsafe DualThreadHandle InitializeRootOutputHandle(
+ ref IRootOutputControlExtensions.JobStruct.ControlStorage storage,
+ ControlHeader* control,
+ ProcessorInstance.InitializationFlags flags
+ )
+ where TRealtime : unmanaged, RootOutputInstance.IRealtime
+ where TControl : unmanaged, RootOutputInstance.IControl
+
+ {
+ fixed (ProcessorHeader* header = &storage.HeaderAndProcessor.Header)
+ return InternalInitializeRootOutputHandle(header, sizeof(IRootOutputControlExtensions.JobStruct.ControlStorage), control, flags);
+ }
// Intermediate above exists because otherwise bindings layer will throw.
[NativeMethod(Name = "audio::InitializeRootOutputHandle", IsFreeFunction = true, ThrowsException = true)]
- static extern unsafe void InternalInitializeRootOutputHandle(/*ScriptingProcessorHeader*/ void* header, /*ControlHeader*/ void* control, ProcessorInstance.InitializationFlags flags);
+ static extern unsafe DualThreadHandle InternalInitializeRootOutputHandle(/*ScriptingProcessorHeader*/ void* header, int tailSize, /*ControlHeader*/ void* control, ProcessorInstance.InitializationFlags flags);
}
#endregion
diff --git a/Modules/BuildAnalysis/Data/Models/BuildAnalysis.cs b/Modules/BuildAnalysis/Data/Models/BuildAnalysis.cs
index 9124f364de..2f5545a646 100644
--- a/Modules/BuildAnalysis/Data/Models/BuildAnalysis.cs
+++ b/Modules/BuildAnalysis/Data/Models/BuildAnalysis.cs
@@ -16,6 +16,28 @@ internal class BuildAnalysis
public BuildAnalysisTables Tables = new BuildAnalysisTables();
public BuildAnalysisMessage[] Messages = Array.Empty();
public BuildAnalysisComputed Computed = new BuildAnalysisComputed();
+
+ // Where the Assets table came from. Populated when this build recorded no assets of its own
+ // (e.g. a scripts-only build) and the data was borrowed from an earlier complete build.
+ public BuildAnalysisAssetSource AssetSource = new BuildAnalysisAssetSource();
+ }
+
+ [Serializable]
+ internal class BuildAnalysisAssetSource
+ {
+ // The complete build this build reused content from (BuildReportSummary.ContentSourceBuildSessionGUID).
+ // Empty for builds that produced their own content; set for asset-less scripts-only / incremental builds.
+ public GUID ContentSourceBuildSessionGUID;
+
+ // The content-source build's start time; set only when that build was found and its assets borrowed.
+ public string BuildStartedAtUtc = string.Empty;
+
+ // The content-source build was found and its assets borrowed.
+ public bool IsBorrowed;
+
+ // A content source was declared but couldn't be resolved (pruned/deleted, or its report missing), so the
+ // reused assets are unknown.
+ public bool SourceUnavailable => !IsBorrowed && !ContentSourceBuildSessionGUID.Empty();
}
[Serializable]
@@ -84,6 +106,7 @@ internal struct BuildAnalysisRootAsset
public ulong DirectSizeBytes;
public int TotalAssetCount;
public ulong TotalSizeBytes;
+ public int[] ReferencedAssetIds;
}
[Serializable]
diff --git a/Modules/BuildAnalysis/Data/Models/RootAssetStats.cs b/Modules/BuildAnalysis/Data/Models/RootAssetStats.cs
index 13721bf8ce..4d79a3c5a6 100644
--- a/Modules/BuildAnalysis/Data/Models/RootAssetStats.cs
+++ b/Modules/BuildAnalysis/Data/Models/RootAssetStats.cs
@@ -19,5 +19,6 @@ internal struct RootAssetStats
public ulong DirectSize;
public int TotalAssets;
public ulong TotalSize;
+ public string[] ReferencedAssetPaths;
}
}
diff --git a/Modules/BuildAnalysis/Services/BuildAnalysisAssembler.cs b/Modules/BuildAnalysis/Services/BuildAnalysisAssembler.cs
new file mode 100644
index 0000000000..b2161c7e85
--- /dev/null
+++ b/Modules/BuildAnalysis/Services/BuildAnalysisAssembler.cs
@@ -0,0 +1,269 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using UnityEngine;
+
+namespace UnityEditor.Build.Analysis
+{
+ internal static class BuildAnalysisAssembler
+ {
+ public static BuildAnalysis Assemble(
+ BuildReportSummary reportSummary,
+ BuildReportData reportData,
+ RootAssetStats[] rootStats,
+ SourceBuildAssets? sourceBuildAssets)
+ {
+ // An asset-less build (e.g. scripts-only) borrows the Assets table from an earlier complete build.
+ var assetData = sourceBuildAssets?.Assets ?? reportData.Assets;
+
+ // The content source this asset-less build declared, recorded even when it can't be resolved so the UI
+ // can tell "reused a build that's gone" from "genuinely has no assets". Empty for builds with own content.
+ var declaredContentSource = reportData.Assets.Length == 0
+ && !reportSummary.ContentSourceBuildSessionGUID.Empty()
+ && reportSummary.ContentSourceBuildSessionGUID != reportSummary.BuildSessionGUID
+ ? reportSummary.ContentSourceBuildSessionGUID
+ : default;
+
+ var stepTable = ConvertSteps(reportData.Steps);
+ var analysisMessages = ConvertMessages(reportData.Messages, stepTable.Length);
+ ConvertAssets(assetData, out var assetTable, out var importerTypeTable);
+ var rootAssetTable = ConvertRootAssets(rootStats, assetTable);
+ var computed = BuildComputed(
+ assetTable,
+ rootAssetTable,
+ analysisMessages,
+ reportData.CachedReusePercent);
+
+ var output = new BuildAnalysis
+ {
+ Version = BuildAnalysisConstants.k_SchemaVersion,
+ GeneratedAtUtc = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture),
+ Summary = new BuildAnalysisSummary
+ {
+ BuildSessionGUID = reportSummary.BuildSessionGUID.ToString(),
+ BuildName = reportSummary.BuildName ?? string.Empty,
+ BuildProfilePath = reportSummary.BuildProfilePath ?? string.Empty,
+ Platform = reportSummary.Platform.ToString(),
+ BuildResult = reportSummary.BuildResult.ToString(),
+ BuildStartedAtUtc = reportSummary.BuildStartedAt ?? string.Empty,
+ BuildType = reportSummary.BuildType.ToString(),
+ TotalSizeBytes = reportSummary.TotalSizeBytes,
+ TotalTimeMs = reportSummary.TotalTimeMs > 0 ? reportSummary.TotalTimeMs : reportData.TotalDurationMs,
+ TotalErrors = reportData.TotalErrors,
+ TotalWarnings = reportData.TotalWarnings,
+ BuildManifestHash = reportSummary.BuildManifestHash ?? string.Empty,
+ OutputPath = reportSummary.OutputPath ?? string.Empty,
+ BuildOptions = reportSummary.BuildOptions ?? Array.Empty(),
+ BuildContentOptions = reportSummary.BuildContentOptions ?? Array.Empty(),
+ },
+ Tables = new BuildAnalysisTables
+ {
+ Steps = stepTable,
+ Assets = assetTable,
+ ImporterTypes = importerTypeTable,
+ RootAssets = rootAssetTable,
+ },
+ Messages = analysisMessages,
+ Computed = computed,
+ AssetSource = BuildAssetSource(sourceBuildAssets, declaredContentSource),
+ };
+
+ return output;
+ }
+
+ private static BuildAnalysisAssetSource BuildAssetSource(SourceBuildAssets? sourceBuildAssets, GUID declaredContentSource)
+ {
+ // Not borrowed: keep the declared source so a still-unresolved one reads as source unavailable.
+ var borrowed = sourceBuildAssets.HasValue;
+ if (!borrowed)
+ return new BuildAnalysisAssetSource { ContentSourceBuildSessionGUID = declaredContentSource };
+
+ var source = sourceBuildAssets.Value;
+ return new BuildAnalysisAssetSource
+ {
+ ContentSourceBuildSessionGUID = source.BuildGuid,
+ BuildStartedAtUtc = source.BuildSummary.BuildStartedAt ?? string.Empty,
+ IsBorrowed = true,
+ };
+ }
+
+ private static BuildAnalysisStep[] ConvertSteps(BuildReportStepData[] steps)
+ {
+ var result = new BuildAnalysisStep[steps.Length];
+ for (var i = 0; i < steps.Length; i++)
+ {
+ var source = steps[i];
+ result[i] = new BuildAnalysisStep
+ {
+ Id = i,
+ Name = source.Name ?? string.Empty,
+ Depth = source.Depth,
+ DurationMs = source.DurationMs,
+ };
+ }
+
+ return result;
+ }
+
+ private static BuildAnalysisMessage[] ConvertMessages(BuildReportMessageData[] messages, int maxStepCount)
+ {
+ var result = new BuildAnalysisMessage[messages.Length];
+
+ for (var i = 0; i < messages.Length; i++)
+ {
+ var source = messages[i];
+ var stepIndex = source.StepIndex;
+ if (stepIndex < 0 || stepIndex >= maxStepCount)
+ stepIndex = -1;
+
+ result[i] = new BuildAnalysisMessage
+ {
+ Severity = source.Severity ?? string.Empty,
+ StepId = stepIndex,
+ Text = source.Content ?? string.Empty,
+ };
+ }
+
+ return result;
+ }
+
+ private static void ConvertAssets(
+ BuildReportAssetData[] sourceAssets,
+ out BuildAnalysisAsset[] assets,
+ out BuildAnalysisImporterType[] importerTypes)
+ {
+ if (sourceAssets.Length == 0)
+ {
+ assets = Array.Empty();
+ importerTypes = Array.Empty();
+ return;
+ }
+
+ var importerIdByName = new Dictionary(StringComparer.Ordinal);
+ var importerList = new List();
+
+ assets = new BuildAnalysisAsset[sourceAssets.Length];
+ for (var i = 0; i < sourceAssets.Length; i++)
+ {
+ var src = sourceAssets[i];
+ var importerKey = string.IsNullOrEmpty(src.ImporterTypeName) ? "Unknown" : src.ImporterTypeName;
+ if (!importerIdByName.TryGetValue(importerKey, out var importerId))
+ {
+ importerId = importerList.Count;
+ importerList.Add(new BuildAnalysisImporterType { Id = importerId, Name = importerKey });
+ importerIdByName[importerKey] = importerId;
+ }
+
+ assets[i] = new BuildAnalysisAsset
+ {
+ Id = i,
+ Path = src.Path ?? string.Empty,
+ GUID = src.GUID,
+ OutputSizeBytes = src.OutputSizeBytes,
+ ObjectCount = src.ObjectCount,
+ ResourceCount = src.ResourceCount,
+ ImporterTypeId = importerId,
+ };
+ }
+
+ importerTypes = importerList.ToArray();
+ }
+
+ private static BuildAnalysisRootAsset[] ConvertRootAssets(
+ RootAssetStats[] rootStats,
+ BuildAnalysisAsset[] assets)
+ {
+ if (rootStats.Length == 0)
+ return Array.Empty();
+
+ var pathToAssetId = new Dictionary(assets.Length, StringComparer.Ordinal);
+ foreach (var a in assets)
+ {
+ if (!string.IsNullOrEmpty(a.Path))
+ pathToAssetId[a.Path] = a.Id;
+ }
+
+ var result = new List(rootStats.Length);
+ foreach (var s in rootStats)
+ {
+ if (string.IsNullOrEmpty(s.AssetPath) || !pathToAssetId.TryGetValue(s.AssetPath, out var assetId))
+ {
+ // Root assets are project source assets that should appear in BuildReport.assetStats.
+ // Skip on the rare miss rather than emit a sentinel AssetId.
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Root asset '{s.AssetPath}' not found in Assets table.");
+ continue;
+ }
+ result.Add(new BuildAnalysisRootAsset
+ {
+ Id = result.Count,
+ AssetId = assetId,
+ DirectAssetCount = s.DirectAssets,
+ DirectSizeBytes = s.DirectSize,
+ TotalAssetCount = s.TotalAssets,
+ TotalSizeBytes = s.TotalSize,
+ ReferencedAssetIds = ResolveReferencedAssetIds(s.ReferencedAssetPaths, pathToAssetId),
+ });
+ }
+ return result.ToArray();
+ }
+
+ private static int[] ResolveReferencedAssetIds(
+ string[] referencedAssetPaths,
+ Dictionary pathToAssetId)
+ {
+ if (referencedAssetPaths == null || referencedAssetPaths.Length == 0)
+ return Array.Empty();
+
+ var ids = new List(referencedAssetPaths.Length);
+ foreach (var path in referencedAssetPaths)
+ {
+ if (pathToAssetId.TryGetValue(path, out var id))
+ ids.Add(id);
+ }
+ return ids.Count == 0 ? Array.Empty() : ids.ToArray();
+ }
+
+ private static BuildAnalysisComputed BuildComputed(
+ BuildAnalysisAsset[] assets,
+ BuildAnalysisRootAsset[] rootAssets,
+ BuildAnalysisMessage[] messages,
+ float cacheReusePercent)
+ {
+ var counts = new BuildAnalysisCounts
+ {
+ AssetCount = assets.Length,
+ RootAssetCount = rootAssets.Length,
+ };
+
+ foreach (var asset in assets)
+ {
+ if (!string.IsNullOrEmpty(asset.Path)
+ && asset.Path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
+ {
+ counts.SceneCount++;
+ }
+ }
+
+ foreach (var t in messages)
+ {
+ var severity = t.Severity;
+ if (string.Equals(severity, BuildMessageSeverity.Error, StringComparison.Ordinal))
+ counts.ErrorMessageCount++;
+ else if (string.Equals(severity, BuildMessageSeverity.Warning, StringComparison.Ordinal))
+ counts.WarningMessageCount++;
+ else
+ counts.InfoMessageCount++;
+ }
+
+ return new BuildAnalysisComputed
+ {
+ Counts = counts,
+ CacheReusePercent = cacheReusePercent,
+ };
+ }
+ }
+}
diff --git a/Modules/BuildAnalysis/Services/BuildAnalysisService.cs b/Modules/BuildAnalysis/Services/BuildAnalysisService.cs
index fbaf7ea3ff..6491e36faa 100644
--- a/Modules/BuildAnalysis/Services/BuildAnalysisService.cs
+++ b/Modules/BuildAnalysis/Services/BuildAnalysisService.cs
@@ -3,7 +3,11 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Unity.Profiling;
using UnityEngine;
namespace UnityEditor.Build.Analysis
@@ -11,30 +15,62 @@ namespace UnityEditor.Build.Analysis
///
/// Main service for build analysis functionality
///
- internal class BuildAnalysisService
+ internal class BuildAnalysisService : IDisposable
{
+ static readonly ProfilerMarker s_LoadFromDiskMarker = new ProfilerMarker("BuildAnalysisService.LoadFromDisk");
+
private readonly IBuildEnumerator m_Enumerator;
private readonly IBuildAnalyzer m_Analyzer;
private readonly IBuildAnalysisFileSystem m_FileSystem;
- private readonly IBuildAnalysisProgressReporter m_ProgressReporter;
private readonly IBuildHistoryProvider m_BuildHistory;
private readonly LRUCache m_Cache;
+ // In-flight de-duplication: a second request for a build that is already loading/generating awaits
+ // the same Task instead of recomputing.
+ private readonly Dictionary> m_InFlight = new Dictionary>();
+ private readonly object m_InFlightLock = new object();
+
+ // Cancels all in-flight work on teardown (window close / domain reload). The window builds a fresh
+ // service per OnEnable, so each session gets a fresh token (no reuse-after-cancel).
+ private readonly CancellationTokenSource m_Cts = new CancellationTokenSource();
+ private bool m_Disposed;
+
public BuildAnalysisService(
IBuildEnumerator enumerator,
IBuildAnalyzer analyzer,
IBuildAnalysisFileSystem fileSystem,
- IBuildAnalysisProgressReporter progressReporter,
IBuildHistoryProvider buildHistory)
{
m_Enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator));
m_Analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
m_FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
- m_ProgressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter));
m_BuildHistory = buildHistory ?? throw new ArgumentNullException(nameof(buildHistory));
m_Cache = new LRUCache(20);
}
+ ///
+ /// Cancel any in-flight analysis without disposing (e.g. before a domain reload, where OnEnable
+ /// rebuilds the service afterwards).
+ ///
+ public void CancelPending()
+ {
+ if (m_Disposed)
+ return;
+ m_Cts.Cancel();
+ }
+
+ ///
+ /// Cancels in-flight analysis and releases the cancellation source. Call on window teardown.
+ ///
+ public void Dispose()
+ {
+ if (m_Disposed)
+ return;
+ m_Disposed = true;
+ m_Cts.Cancel();
+ m_Cts.Dispose();
+ }
+
///
/// Refresh the build history state from disk, then clear the cache.
/// Call this before GetBuilds when an explicit user-initiated refresh is needed.
@@ -102,57 +138,38 @@ public void DeleteAllBuilds()
}
///
- /// Get analysis for a specific build
+ /// Get analysis for a specific build, off the main thread. Returns the cached instance synchronously
+ /// when available, joins an in-flight load/generation when one exists, otherwise starts one.
///
- public BuildAnalysis GetBuildAnalysis(GUID buildSessionGUID)
+ public Task GetBuildAnalysisAsync(GUID buildSessionGUID)
{
if (buildSessionGUID.Empty())
throw new ArgumentException("BuildSessionGUID is empty.", nameof(buildSessionGUID));
- var cachedAnalysis = m_Cache.Get(buildSessionGUID);
- if (cachedAnalysis != null)
- return cachedAnalysis;
+ var cached = m_Cache.Get(buildSessionGUID);
+ if (cached != null)
+ return Task.FromResult(cached);
- if (!m_Enumerator.TryGetBuild(buildSessionGUID, out var entry))
+ lock (m_InFlightLock)
{
- Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} No build found for BuildSessionGUID '{buildSessionGUID}'.");
- return null;
+ if (m_InFlight.TryGetValue(buildSessionGUID, out var inflight))
+ return inflight;
}
- try
- {
- BuildAnalysis analysis;
- if (TryGetBuildAnalysisPath(buildSessionGUID, out var analysisPath))
- {
- analysis = LoadBuildAnalysisFromDisk(analysisPath);
- }
- else
- {
- m_ProgressReporter.Show("Build Analysis", $"Generating analysis for '{entry.BuildName}'...", 0.5f);
- try
- {
- analysis = m_Analyzer.Generate(entry);
- }
- finally
- {
- m_ProgressReporter.Clear();
- }
- }
-
- m_Cache.Put(buildSessionGUID, analysis);
- return analysis;
- }
- catch (Exception e)
+ if (!m_Enumerator.TryGetBuild(buildSessionGUID, out var entry))
{
- Debug.LogError($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to get analysis for '{buildSessionGUID}': {e.Message}");
- return null;
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} No build found for BuildSessionGUID '{buildSessionGUID}'.");
+ return Task.FromResult(null);
}
+
+ return Register(buildSessionGUID, () => LoadOrGenerateAsync(buildSessionGUID, entry, regenerate: false));
}
///
- /// Force regeneration of BuildAnalysis.json for the given build and update cache.
+ /// Force regeneration of BuildAnalysis.json for the given build and update the cache. Bypasses both the
+ /// memory cache and the on-disk file, and supersedes any in-flight load for the same build.
///
- public BuildAnalysis RegenerateBuildAnalysis(GUID buildSessionGUID)
+ public Task RegenerateBuildAnalysisAsync(GUID buildSessionGUID)
{
if (buildSessionGUID.Empty())
throw new ArgumentException("BuildSessionGUID is empty.", nameof(buildSessionGUID));
@@ -160,17 +177,9 @@ public BuildAnalysis RegenerateBuildAnalysis(GUID buildSessionGUID)
if (!m_Enumerator.TryGetBuild(buildSessionGUID, out var entry))
throw new ArgumentException($"No build found for BuildSessionGUID '{buildSessionGUID}'.", nameof(buildSessionGUID));
- m_ProgressReporter.Show("Build Analysis", $"Regenerating analysis for '{entry.BuildName}'...", 0.5f);
- try
- {
- var analysis = m_Analyzer.Generate(entry);
- m_Cache.Put(buildSessionGUID, analysis);
- return analysis;
- }
- finally
- {
- m_ProgressReporter.Clear();
- }
+ // Invalidate first so a concurrent GetBuildAnalysisAsync can't serve the stale entry we're replacing.
+ m_Cache.Remove(buildSessionGUID);
+ return Register(buildSessionGUID, () => LoadOrGenerateAsync(buildSessionGUID, entry, regenerate: true));
}
///
@@ -189,6 +198,71 @@ public bool HasBuildAnalysis(GUID buildSessionGUID)
return TryGetBuildAnalysisPath(buildSessionGUID, out _);
}
+ // Registers a freshly-started task as the in-flight entry for a build, and removes it on completion
+ private Task Register(GUID guid, Func> factory)
+ {
+ Task task = null;
+
+ async Task Tracked()
+ {
+ try
+ {
+ return await factory();
+ }
+ finally
+ {
+ lock (m_InFlightLock)
+ {
+ if (m_InFlight.TryGetValue(guid, out var current) && ReferenceEquals(current, task))
+ m_InFlight.Remove(guid);
+ }
+ }
+ }
+
+ task = Tracked();
+ if (!task.IsCompleted)
+ {
+ lock (m_InFlightLock)
+ m_InFlight[guid] = task;
+ }
+ return task;
+ }
+
+ private async Task LoadOrGenerateAsync(GUID guid, BuildEntry entry, bool regenerate)
+ {
+ try
+ {
+ BuildAnalysis analysis = null;
+ if (!regenerate && TryGetBuildAnalysisPath(guid, out var analysisPath))
+ {
+ var cached = await Task.Run(() => LoadBuildAnalysisFromDisk(analysisPath), m_Cts.Token);
+
+ // Only serve a cache written by the current schema. If not regenerate from the source
+ // BuildReport instead of serving stale data. Keeping this a simple version
+ // compare makes every future schema bump self-healing.
+ // If the source report has since been pruned, GenerateAsync throws and the catch below returns
+ // null; that rare stale-cache-without-report case degrades to empty, which is acceptable.
+ if (cached.Version == BuildAnalysisConstants.k_SchemaVersion)
+ analysis = cached;
+ }
+
+ if (analysis == null)
+ analysis = await m_Analyzer.GenerateAsync(entry, m_Cts.Token);
+
+ m_Cache.Put(guid, analysis);
+ return analysis;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to get analysis for '{guid}': {e.Message}");
+ return null;
+ }
+ }
+
private bool TryGetBuildAnalysisPath(GUID buildSessionGUID, out string path)
{
return m_BuildHistory.TryGetFilePath(buildSessionGUID, BuildAnalysisConstants.k_BuildAnalysisRelativePath, out path);
@@ -196,13 +270,16 @@ private bool TryGetBuildAnalysisPath(GUID buildSessionGUID, out string path)
private BuildAnalysis LoadBuildAnalysisFromDisk(string analysisPath)
{
- var json = m_FileSystem.ReadAllText(analysisPath);
- if (string.IsNullOrWhiteSpace(json))
- throw new InvalidDataException($"Build analysis file is empty: '{analysisPath}'.");
+ using (s_LoadFromDiskMarker.Auto())
+ {
+ var json = m_FileSystem.ReadAllText(analysisPath);
+ if (string.IsNullOrWhiteSpace(json))
+ throw new InvalidDataException($"Build analysis file is empty: '{analysisPath}'.");
- var analysis = JsonUtility.FromJson(json);
- ValidateBuildAnalysis(analysis, analysisPath);
- return analysis;
+ var analysis = JsonUtility.FromJson(json);
+ ValidateBuildAnalysis(analysis, analysisPath);
+ return analysis;
+ }
}
private static void ValidateBuildAnalysis(BuildAnalysis analysis, string path)
@@ -215,23 +292,4 @@ private static void ValidateBuildAnalysis(BuildAnalysis analysis, string path)
throw new InvalidDataException($"Build analysis has invalid Version '{analysis.Version}' in '{path}'.");
}
}
-
- internal interface IBuildAnalysisProgressReporter
- {
- void Show(string title, string info, float progress);
- void Clear();
- }
-
- internal sealed class BuildAnalysisProgressReporter : IBuildAnalysisProgressReporter
- {
- public void Show(string title, string info, float progress)
- {
- EditorUtility.DisplayProgressBar(title, info, progress);
- }
-
- public void Clear()
- {
- EditorUtility.ClearProgressBar();
- }
- }
}
diff --git a/Modules/BuildAnalysis/Services/BuildAnalyzer.cs b/Modules/BuildAnalysis/Services/BuildAnalyzer.cs
index 4bbe040883..4c4da3a6a6 100644
--- a/Modules/BuildAnalysis/Services/BuildAnalyzer.cs
+++ b/Modules/BuildAnalysis/Services/BuildAnalyzer.cs
@@ -3,9 +3,10 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
-using System.Collections.Generic;
-using System.Globalization;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Unity.Profiling;
using UnityEditor.Build.Reporting;
using UnityEngine;
@@ -13,282 +14,174 @@ namespace UnityEditor.Build.Analysis
{
internal interface IBuildAnalyzer
{
- BuildAnalysis Generate(BuildEntry entry);
+ Task GenerateAsync(BuildEntry entry, CancellationToken ct);
}
internal sealed class BuildAnalyzer : IBuildAnalyzer
{
- private const int k_SchemaVersion = 1;
+ static readonly ProfilerMarker s_GenerateMarker = new ProfilerMarker("BuildAnalyzer.Generate");
+ static readonly ProfilerMarker s_LoadBuildReportMarker = new ProfilerMarker("BuildAnalyzer.LoadBuildReport");
+ static readonly ProfilerMarker s_ParseContentLayoutMarker = new ProfilerMarker("BuildAnalyzer.ParseContentLayout");
+ static readonly ProfilerMarker s_AssembleMarker = new ProfilerMarker("BuildAnalyzer.Assemble");
+ static readonly ProfilerMarker s_SerializeMarker = new ProfilerMarker("BuildAnalyzer.Serialize");
+ static readonly ProfilerMarker s_WriteMarker = new ProfilerMarker("BuildAnalyzer.Write");
private readonly IBuildReportConverter m_BuildReportConverter;
private readonly IBuildAnalysisFileSystem m_FileSystem;
private readonly IBuildHistoryProvider m_BuildHistory;
+ private readonly ISourceBuildAssetResolver m_AssetResolver;
public BuildAnalyzer(
IBuildReportConverter buildReportConverter,
IBuildAnalysisFileSystem fileSystem,
- IBuildHistoryProvider buildHistory)
+ IBuildHistoryProvider buildHistory,
+ ISourceBuildAssetResolver assetResolver)
{
m_BuildReportConverter = buildReportConverter ?? throw new ArgumentNullException(nameof(buildReportConverter));
m_FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
m_BuildHistory = buildHistory ?? throw new ArgumentNullException(nameof(buildHistory));
+ m_AssetResolver = assetResolver ?? throw new ArgumentNullException(nameof(assetResolver));
}
- public BuildAnalysis Generate(BuildEntry entry)
+ private readonly struct GatheredInputs
{
- ValidateEntry(entry);
-
- var reportSummary = m_BuildHistory.GetBuildSummary(entry.BuildSessionGUID);
-
- if (!m_BuildHistory.TryLoadBuildReport(entry.BuildSessionGUID, out var buildReport))
- throw new InvalidDataException($"Missing build report for build '{entry.BuildSessionGUID}'.");
- var reportData = m_BuildReportConverter.Convert(buildReport);
-
- if (!m_BuildHistory.TryGetBuildReportDirectory(entry.BuildSessionGUID, out var metadataPath))
- throw new InvalidDataException($"No build report directory available for build '{entry.BuildSessionGUID}'.");
-
- var rootStats = reportSummary.BuildType == BuildType.ContentDirectory
- ? LoadRootAssetStats(metadataPath)
- : Array.Empty();
-
- var analysis = BuildAnalysisFrom(reportSummary, reportData, rootStats);
+ public readonly BuildReportSummary ReportSummary;
+ public readonly BuildReportData ReportData;
+ public readonly string MetadataPath;
+ public readonly SourceBuildAssets? SourceBuildAssets;
- var analysisPath = Path.Combine(metadataPath, BuildAnalysisConstants.k_BuildAnalysisRelativePath);
- var json = JsonUtility.ToJson(analysis, true);
- m_FileSystem.WriteAllText(analysisPath, json);
-
- return analysis;
- }
-
- private RootAssetStats[] LoadRootAssetStats(string metadataPath)
- {
- var contentLayoutPath = Path.Combine(metadataPath, BuildAnalysisConstants.k_ContentLayoutFileName);
- if (!m_FileSystem.Exists(contentLayoutPath))
+ public GatheredInputs(BuildReportSummary reportSummary, BuildReportData reportData, string metadataPath, SourceBuildAssets? sourceBuildAssets)
{
- Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} ContentLayout.json not found at '{contentLayoutPath}'. RootAssets will be empty.");
- return Array.Empty();
+ ReportSummary = reportSummary;
+ ReportData = reportData;
+ MetadataPath = metadataPath;
+ SourceBuildAssets = sourceBuildAssets;
}
+ }
- try
- {
- // FromJson is preferred over ContentLayout.Load so all I/O stays behind
- // IBuildAnalysisFileSystem (testable). FromJson still emits the version-mismatch warning.
- var layout = ContentLayout.FromJson(m_FileSystem.ReadAllText(contentLayoutPath));
- if (layout == null)
- return Array.Empty();
- return RootAssetStatsCalculator.Calculate(layout);
- }
- catch (Exception e)
+ ///
+ /// Synchronous composition of the same three stages as . Not on
+ /// and not called in production (the UI uses );
+ /// it exists as the deterministic test seam for the full pipeline.
+ ///
+ public BuildAnalysis Generate(BuildEntry entry)
+ {
+ using (s_GenerateMarker.Auto())
{
- Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to read or parse ContentLayout.json at '{contentLayoutPath}': {e.Message}");
- return Array.Empty();
+ var inputs = GatherMainThreadInputs(entry);
+ var analysis = AssembleAnalysis(inputs);
+ PersistAnalysis(analysis, inputs.MetadataPath);
+ return analysis;
}
}
- private static BuildAnalysis BuildAnalysisFrom(BuildReportSummary reportSummary, BuildReportData reportData, RootAssetStats[] rootStats)
+ ///
+ /// Async generation: native build-report access stays on the main thread (pre-await); the heavy
+ /// pure-managed work (ContentLayout parse + root-asset BFS + assembly) runs on a background thread;
+ /// the disk cache is written fire-and-forget so the UI never waits on serialization.
+ ///
+ public async Task GenerateAsync(BuildEntry entry, CancellationToken ct)
{
- var stepTable = ConvertSteps(reportData.Steps);
- var analysisMessages = ConvertMessages(reportData.Messages, stepTable.Length);
- ConvertAssets(reportData.Assets, out var assetTable, out var importerTypeTable);
- var rootAssetTable = ConvertRootAssets(rootStats, assetTable);
- var computed = BuildComputed(
- assetTable,
- rootAssetTable,
- analysisMessages,
- reportData.CachedReusePercent);
+ // Already torn down before we started: skip the native gather entirely.
+ ct.ThrowIfCancellationRequested();
- var output = new BuildAnalysis
- {
- Version = k_SchemaVersion,
- GeneratedAtUtc = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture),
- Summary = new BuildAnalysisSummary
- {
- BuildSessionGUID = reportSummary.BuildSessionGUID.ToString(),
- BuildName = reportSummary.BuildName ?? string.Empty,
- BuildProfilePath = reportSummary.BuildProfilePath ?? string.Empty,
- Platform = reportSummary.Platform.ToString(),
- BuildResult = reportSummary.BuildResult.ToString(),
- BuildStartedAtUtc = reportSummary.BuildStartedAt ?? string.Empty,
- BuildType = reportSummary.BuildType.ToString(),
- TotalSizeBytes = reportSummary.TotalSizeBytes,
- TotalTimeMs = reportSummary.TotalTimeMs > 0 ? reportSummary.TotalTimeMs : reportData.TotalDurationMs,
- TotalErrors = reportData.TotalErrors,
- TotalWarnings = reportData.TotalWarnings,
- BuildManifestHash = reportSummary.BuildManifestHash ?? string.Empty,
- OutputPath = reportSummary.OutputPath ?? string.Empty,
- BuildOptions = reportSummary.BuildOptions ?? Array.Empty(),
- BuildContentOptions = reportSummary.BuildContentOptions ?? Array.Empty(),
- },
- Tables = new BuildAnalysisTables
- {
- Steps = stepTable,
- Assets = assetTable,
- ImporterTypes = importerTypeTable,
- RootAssets = rootAssetTable,
- },
- Messages = analysisMessages,
- Computed = computed,
- };
+ // Main thread (pre-await): native BuildReport load + convert + AssetDatabase importer lookup.
+ var inputs = GatherMainThreadInputs(entry);
- return output;
- }
+ // Off the main thread: all pure managed transform.
+ var analysis = await Task.Run(() => AssembleAnalysis(inputs), ct);
- private static BuildAnalysisStep[] ConvertSteps(BuildReportStepData[] steps)
- {
- var result = new BuildAnalysisStep[steps.Length];
- for (var i = 0; i < steps.Length; i++)
+ // Background, fire-and-forget: persisting the cache is not on the time-to-interactive path and is
+ // intentionally not tied to ct. A build the user navigated away from is still worth caching.
+ var metadataPath = inputs.MetadataPath;
+ var guid = entry.BuildSessionGUID;
+ _ = Task.Run(() =>
{
- var source = steps[i];
- result[i] = new BuildAnalysisStep
+ try
{
- Id = i,
- Name = source.Name ?? string.Empty,
- Depth = source.Depth,
- DurationMs = source.DurationMs,
- };
- }
+ PersistAnalysis(analysis, metadataPath);
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to persist analysis for '{guid}': {e.Message}");
+ }
+ });
- return result;
+ return analysis;
}
- private static BuildAnalysisMessage[] ConvertMessages(BuildReportMessageData[] messages, int maxStepCount)
+ private GatheredInputs GatherMainThreadInputs(BuildEntry entry)
{
- var result = new BuildAnalysisMessage[messages.Length];
-
- for (var i = 0; i < messages.Length; i++)
- {
- var source = messages[i];
- var stepIndex = source.StepIndex;
- if (stepIndex < 0 || stepIndex >= maxStepCount)
- stepIndex = -1;
-
- result[i] = new BuildAnalysisMessage
- {
- Severity = source.Severity ?? string.Empty,
- StepId = stepIndex,
- Text = source.Content ?? string.Empty,
- };
- }
+ ValidateEntry(entry);
- return result;
- }
+ var reportSummary = m_BuildHistory.GetBuildSummary(entry.BuildSessionGUID);
- private static void ConvertAssets(
- BuildReportAssetData[] sourceAssets,
- out BuildAnalysisAsset[] assets,
- out BuildAnalysisImporterType[] importerTypes)
- {
- if (sourceAssets.Length == 0)
+ BuildReport buildReport;
+ using (s_LoadBuildReportMarker.Auto())
{
- assets = Array.Empty();
- importerTypes = Array.Empty();
- return;
+ if (!m_BuildHistory.TryLoadBuildReport(entry.BuildSessionGUID, out buildReport))
+ throw new InvalidDataException($"Missing build report for build '{entry.BuildSessionGUID}'.");
}
+ var reportData = m_BuildReportConverter.Convert(buildReport);
- var importerIdByName = new Dictionary(StringComparer.Ordinal);
- var importerList = new List();
-
- assets = new BuildAnalysisAsset[sourceAssets.Length];
- for (var i = 0; i < sourceAssets.Length; i++)
- {
- var src = sourceAssets[i];
- var importerKey = string.IsNullOrEmpty(src.ImporterTypeName) ? "Unknown" : src.ImporterTypeName;
- if (!importerIdByName.TryGetValue(importerKey, out var importerId))
- {
- importerId = importerList.Count;
- importerList.Add(new BuildAnalysisImporterType { Id = importerId, Name = importerKey });
- importerIdByName[importerKey] = importerId;
- }
+ // A Player build that recorded no assets (scripts-only, or an incremental build that reused its data
+ // cache) borrows the asset table from the exact source build the pipeline recorded on this build's
+ // summary. The resolver limits itself to Player builds, so no build-type check is needed here.
+ SourceBuildAssets? sourceBuildAssets = null;
+ if (reportData.Assets.Length == 0 && m_AssetResolver.TryResolveSourceBuildAssets(reportSummary, out var resolved))
+ sourceBuildAssets = resolved;
- assets[i] = new BuildAnalysisAsset
- {
- Id = i,
- Path = src.Path ?? string.Empty,
- GUID = src.GUID,
- OutputSizeBytes = src.OutputSizeBytes,
- ObjectCount = src.ObjectCount,
- ResourceCount = src.ResourceCount,
- ImporterTypeId = importerId,
- };
- }
+ if (!m_BuildHistory.TryGetBuildReportDirectory(entry.BuildSessionGUID, out var metadataPath))
+ throw new InvalidDataException($"No build report directory available for build '{entry.BuildSessionGUID}'.");
- importerTypes = importerList.ToArray();
+ return new GatheredInputs(reportSummary, reportData, metadataPath, sourceBuildAssets);
}
- private static BuildAnalysisRootAsset[] ConvertRootAssets(
- RootAssetStats[] rootStats,
- BuildAnalysisAsset[] assets)
+ private BuildAnalysis AssembleAnalysis(GatheredInputs inputs)
{
- if (rootStats.Length == 0)
- return Array.Empty();
-
- var pathToAssetId = new Dictionary(assets.Length, StringComparer.Ordinal);
- foreach (var a in assets)
- {
- if (!string.IsNullOrEmpty(a.Path))
- pathToAssetId[a.Path] = a.Id;
- }
+ var rootStats = inputs.ReportSummary.BuildType == BuildType.ContentDirectory
+ ? LoadRootAssetStats(inputs.MetadataPath)
+ : Array.Empty();
- var result = new List(rootStats.Length);
- foreach (var s in rootStats)
- {
- if (string.IsNullOrEmpty(s.AssetPath) || !pathToAssetId.TryGetValue(s.AssetPath, out var assetId))
- {
- // Root assets are project source assets that should appear in BuildReport.assetStats.
- // Skip on the rare miss rather than emit a sentinel AssetId.
- Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Root asset '{s.AssetPath}' not found in Assets table.");
- continue;
- }
- result.Add(new BuildAnalysisRootAsset
- {
- Id = result.Count,
- AssetId = assetId,
- DirectAssetCount = s.DirectAssets,
- DirectSizeBytes = s.DirectSize,
- TotalAssetCount = s.TotalAssets,
- TotalSizeBytes = s.TotalSize,
- });
- }
- return result.ToArray();
+ using (s_AssembleMarker.Auto())
+ return BuildAnalysisAssembler.Assemble(inputs.ReportSummary, inputs.ReportData, rootStats, inputs.SourceBuildAssets);
}
- private static BuildAnalysisComputed BuildComputed(
- BuildAnalysisAsset[] assets,
- BuildAnalysisRootAsset[] rootAssets,
- BuildAnalysisMessage[] messages,
- float cacheReusePercent)
+ private void PersistAnalysis(BuildAnalysis analysis, string metadataPath)
{
- var counts = new BuildAnalysisCounts
- {
- AssetCount = assets.Length,
- RootAssetCount = rootAssets.Length,
- };
+ var analysisPath = Path.Combine(metadataPath, BuildAnalysisConstants.k_BuildAnalysisRelativePath);
+ string json;
+ using (s_SerializeMarker.Auto())
+ json = JsonUtility.ToJson(analysis, false);
+ using (s_WriteMarker.Auto())
+ m_FileSystem.WriteAllText(analysisPath, json);
+ }
- foreach (var asset in assets)
+ private RootAssetStats[] LoadRootAssetStats(string metadataPath)
+ {
+ var contentLayoutPath = Path.Combine(metadataPath, BuildAnalysisConstants.k_ContentLayoutFileName);
+ if (!m_FileSystem.Exists(contentLayoutPath))
{
- if (!string.IsNullOrEmpty(asset.Path)
- && asset.Path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
- {
- counts.SceneCount++;
- }
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} ContentLayout.json not found at '{contentLayoutPath}'. RootAssets will be empty.");
+ return Array.Empty();
}
- foreach (var t in messages)
+ try
{
- var severity = t.Severity;
- if (string.Equals(severity, BuildMessageSeverity.Error, StringComparison.Ordinal))
- counts.ErrorMessageCount++;
- else if (string.Equals(severity, BuildMessageSeverity.Warning, StringComparison.Ordinal))
- counts.WarningMessageCount++;
- else
- counts.InfoMessageCount++;
+ // FromJson is preferred over ContentLayout.Load so all I/O stays behind
+ // IBuildAnalysisFileSystem (testable). FromJson still emits the version-mismatch warning.
+ ContentLayout layout;
+ using (s_ParseContentLayoutMarker.Auto())
+ layout = ContentLayout.FromJson(m_FileSystem.ReadAllText(contentLayoutPath));
+ if (layout == null)
+ return Array.Empty();
+ return RootAssetStatsCalculator.Calculate(layout);
}
-
- return new BuildAnalysisComputed
+ catch (Exception e)
{
- Counts = counts,
- CacheReusePercent = cacheReusePercent,
- };
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to read or parse ContentLayout.json at '{contentLayoutPath}': {e.Message}");
+ return Array.Empty();
+ }
}
private static void ValidateEntry(BuildEntry entry)
diff --git a/Modules/BuildAnalysis/Services/BuildEnumerator.cs b/Modules/BuildAnalysis/Services/BuildEnumerator.cs
index 4be34afb30..b6fb54a665 100644
--- a/Modules/BuildAnalysis/Services/BuildEnumerator.cs
+++ b/Modules/BuildAnalysis/Services/BuildEnumerator.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using UnityEngine;
@@ -95,8 +94,8 @@ private BuildEntry BuildEntryFromSummary(BuildReportSummary summary, GUID guid)
private static DateTime ParseBuildStartedAtLocal(string buildStartedAt)
{
- if (DateTimeOffset.TryParseExact(buildStartedAt, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedDateTime))
- return parsedDateTime.ToLocalTime().DateTime;
+ if (FormatUtility.TryParseBuildTimestamp(buildStartedAt, out var parsed))
+ return parsed.ToLocalTime().DateTime;
throw new FormatException($"Invalid BuildStartedAt value: '{buildStartedAt}'.");
}
diff --git a/Modules/BuildAnalysis/Services/BuildReportConverter.cs b/Modules/BuildAnalysis/Services/BuildReportConverter.cs
index 18e14cacce..ecab3d79a6 100644
--- a/Modules/BuildAnalysis/Services/BuildReportConverter.cs
+++ b/Modules/BuildAnalysis/Services/BuildReportConverter.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Profiling;
using UnityEditor.Build.Reporting;
using UnityEngine;
@@ -16,6 +17,9 @@ internal interface IBuildReportConverter
internal sealed class BuildReportConverter : IBuildReportConverter
{
+ static readonly ProfilerMarker s_ExtractAssetsMarker = new ProfilerMarker("BuildReportConverter.ExtractAssets");
+ static readonly ProfilerMarker s_GetImporterTypesMarker = new ProfilerMarker("BuildReportConverter.GetImporterTypes");
+
public BuildReportData Convert(BuildReport buildReport)
{
if (buildReport == null)
@@ -91,40 +95,45 @@ private static float ComputeCachedReusePercent(BuildReport buildReport)
private static BuildReportAssetData[] ExtractAssets(BuildReport buildReport)
{
- if (buildReport == null)
- return Array.Empty();
+ using (s_ExtractAssetsMarker.Auto())
+ {
+ if (buildReport == null)
+ return Array.Empty();
- var contentSummary = buildReport.contentSummary;
- if (contentSummary == null)
- return Array.Empty();
+ var contentSummary = buildReport.contentSummary;
+ if (contentSummary == null)
+ return Array.Empty();
- var assetStats = contentSummary.assetStats;
- if (assetStats.Length == 0)
- return Array.Empty();
+ var assetStats = contentSummary.assetStats;
+ if (assetStats.Length == 0)
+ return Array.Empty();
- var guids = new GUID[assetStats.Length];
- for (var i = 0; i < assetStats.Length; i++)
- guids[i] = assetStats[i].sourceAssetGUID;
+ var guids = new GUID[assetStats.Length];
+ for (var i = 0; i < assetStats.Length; i++)
+ guids[i] = assetStats[i].sourceAssetGUID;
- var importerTypes = AssetDatabase.GetImporterTypes(guids);
- Debug.Assert(importerTypes.Length == assetStats.Length);
+ Type[] importerTypes;
+ using (s_GetImporterTypesMarker.Auto())
+ importerTypes = AssetDatabase.GetImporterTypes(guids);
+ Debug.Assert(importerTypes.Length == assetStats.Length);
- var assets = new BuildReportAssetData[assetStats.Length];
- for (var i = 0; i < assetStats.Length; i++)
- {
- var stats = assetStats[i];
- assets[i] = new BuildReportAssetData
+ var assets = new BuildReportAssetData[assetStats.Length];
+ for (var i = 0; i < assetStats.Length; i++)
{
- Path = stats.sourceAssetPath ?? string.Empty,
- GUID = stats.sourceAssetGUID,
- OutputSizeBytes = stats.size,
- ObjectCount = stats.objectCount,
- ResourceCount = stats.resourceCount,
- ImporterTypeName = importerTypes[i]?.Name,
- };
- }
+ var stats = assetStats[i];
+ assets[i] = new BuildReportAssetData
+ {
+ Path = stats.sourceAssetPath ?? string.Empty,
+ GUID = stats.sourceAssetGUID,
+ OutputSizeBytes = stats.size,
+ ObjectCount = stats.objectCount,
+ ResourceCount = stats.resourceCount,
+ ImporterTypeName = importerTypes[i]?.Name,
+ };
+ }
- return assets;
+ return assets;
+ }
}
private static string ToSeverityString(LogType messageType)
diff --git a/Modules/BuildAnalysis/Services/RootAssetStatsCalculator.cs b/Modules/BuildAnalysis/Services/RootAssetStatsCalculator.cs
index fdb43bc2b4..4dd871f404 100644
--- a/Modules/BuildAnalysis/Services/RootAssetStatsCalculator.cs
+++ b/Modules/BuildAnalysis/Services/RootAssetStatsCalculator.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Unity.Profiling;
using UnityEngine;
namespace UnityEditor.Build.Analysis
@@ -15,13 +16,16 @@ namespace UnityEditor.Build.Analysis
///
internal static class RootAssetStatsCalculator
{
+ static readonly ProfilerMarker s_CalculateMarker = new ProfilerMarker("RootAssetStatsCalculator.Calculate");
+
public static RootAssetStats[] Calculate(ContentLayout layout)
{
if (layout == null)
throw new ArgumentNullException(nameof(layout));
try
{
- return new Context(layout).Calculate();
+ using (s_CalculateMarker.Auto())
+ return new Context(layout).Calculate();
}
catch (Exception e)
{
@@ -102,6 +106,14 @@ public RootAssetStats[] Calculate()
var direct = Traverse(rootSfIndex, includeLoadables: false);
var total = Traverse(rootSfIndex, includeLoadables: true);
+ // References mirror the Total reachable set: after the includeLoadables pass,
+ // m_UniqueSources holds the root's full reachable closure. Drop the root's own
+ // path and copy the rest now, before the next root's Traverse clears the set.
+ if (assetPath != null)
+ m_UniqueSources.Remove(assetPath);
+ var references = new string[m_UniqueSources.Count];
+ m_UniqueSources.CopyTo(references);
+
results.Add(new RootAssetStats
{
AssetPath = assetPath ?? string.Empty,
@@ -109,6 +121,7 @@ public RootAssetStats[] Calculate()
DirectSize = direct.SizeBytes,
TotalAssets = total.AssetCount,
TotalSize = total.SizeBytes,
+ ReferencedAssetPaths = references,
});
}
return results.ToArray();
diff --git a/Modules/BuildAnalysis/Services/SourceBuildAssetResolver.cs b/Modules/BuildAnalysis/Services/SourceBuildAssetResolver.cs
new file mode 100644
index 0000000000..5aa9ac2aa6
--- /dev/null
+++ b/Modules/BuildAnalysis/Services/SourceBuildAssetResolver.cs
@@ -0,0 +1,102 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using System;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+
+namespace UnityEditor.Build.Analysis
+{
+ ///
+ /// The assets of a previous complete Player build, used to stand in for an asset-less Player build
+ /// (a scripts-only build, or an incremental build that reused its data cache and recorded no assets).
+ ///
+ internal readonly struct SourceBuildAssets
+ {
+ /// The source build the assets came from.
+ public readonly GUID BuildGuid;
+
+ /// Summary of the source build (build name, start time, …).
+ public readonly BuildReportSummary BuildSummary;
+
+ /// The source build's asset rows; may be empty if the source recorded none.
+ public readonly BuildReportAssetData[] Assets;
+
+ public SourceBuildAssets(GUID buildGuid, BuildReportSummary buildSummary, BuildReportAssetData[] assets)
+ {
+ BuildGuid = buildGuid;
+ BuildSummary = buildSummary;
+ Assets = assets;
+ }
+ }
+
+ internal interface ISourceBuildAssetResolver
+ {
+ ///
+ /// Follows the source-build pointer the build pipeline stamped for an asset-less
+ /// and loads that exact source build's assets. No history scan — a result exists only when a valid pointer does.
+ ///
+ ///
+ /// Main-thread only: this loads build reports and runs the importer lookup inside
+ /// , both of which require the main thread.
+ ///
+ ///
+ /// True with set (its asset rows may be empty) when the target has a
+ /// recorded source build whose report still loads; false otherwise.
+ ///
+ bool TryResolveSourceBuildAssets(BuildReportSummary target, out SourceBuildAssets sourceBuildAssets);
+ }
+
+ ///
+ /// Resolves the exact previous build whose data cache an asset-less Player build reused, by following the
+ /// pointer the build pipeline stamps into the build's metadata folder
+ ///
+ internal sealed class SourceBuildAssetResolver : ISourceBuildAssetResolver
+ {
+ readonly IBuildHistoryProvider m_BuildHistory;
+ readonly IBuildReportConverter m_BuildReportConverter;
+
+ public SourceBuildAssetResolver(IBuildHistoryProvider buildHistory, IBuildReportConverter buildReportConverter)
+ {
+ m_BuildHistory = buildHistory ?? throw new ArgumentNullException(nameof(buildHistory));
+ m_BuildReportConverter = buildReportConverter ?? throw new ArgumentNullException(nameof(buildReportConverter));
+ }
+
+ public bool TryResolveSourceBuildAssets(BuildReportSummary target, out SourceBuildAssets sourceBuildAssets)
+ {
+ sourceBuildAssets = default;
+
+ // Only Player builds source assets from another build. ContentDirectory builds pack their own content.
+ if (target.BuildType != BuildType.Player)
+ return false;
+
+ // The build pipeline records the exact source build whose content this one reused, on the summary.
+ // An empty value means the build produced its own (empty) content. The target's own BuildResult is
+ // intentionally not gated: an asset-less Player build shows whatever content it actually reused, the
+ // same way a build's own assets are shown regardless of result (the header still flags the failure).
+ var sourceGuid = target.ContentSourceBuildSessionGUID;
+ if (sourceGuid.Empty() || sourceGuid == target.BuildSessionGUID)
+ return false;
+
+ BuildReportSummary sourceSummary;
+ try
+ {
+ sourceSummary = m_BuildHistory.GetBuildSummary(sourceGuid);
+ }
+ catch (ArgumentException)
+ {
+ // Source build was pruned from history since the pointer was written.
+ return false;
+ }
+
+ // Borrow the source recorded, even an empty table - its assets are exactly the content the target reused.
+ if (!m_BuildHistory.TryLoadBuildReport(sourceGuid, out var report))
+ return false;
+
+ var data = m_BuildReportConverter.Convert(report);
+ sourceBuildAssets = new SourceBuildAssets(sourceGuid, sourceSummary, data.Assets);
+ return true;
+ }
+ }
+}
diff --git a/Modules/BuildAnalysis/UI/BuildAnalysisTabHost.cs b/Modules/BuildAnalysis/UI/BuildAnalysisTabHost.cs
index d97ed453e5..2b6903492f 100644
--- a/Modules/BuildAnalysis/UI/BuildAnalysisTabHost.cs
+++ b/Modules/BuildAnalysis/UI/BuildAnalysisTabHost.cs
@@ -4,12 +4,15 @@
using System;
using System.Collections.Generic;
+using Unity.Profiling;
using UnityEngine.UIElements;
namespace UnityEditor.Build.Analysis
{
internal class BuildAnalysisTabHost
{
+ static readonly ProfilerMarker s_SetSelectionMarker = new ProfilerMarker("BuildAnalysisTabHost.SetSelection");
+
private readonly TabView m_TabView;
private readonly List m_TabRegistrations = new List();
@@ -51,8 +54,11 @@ public void Register(Tab tab, IBuildAnalysisTabView tabView)
public void SetSelection(BuildEntry selection, BuildAnalysis analysis)
{
- foreach (var registration in m_TabRegistrations)
- registration.TabView.SetSelection(selection, analysis);
+ using (s_SetSelectionMarker.Auto())
+ {
+ foreach (var registration in m_TabRegistrations)
+ registration.TabView.SetSelection(selection, analysis);
+ }
}
public void NotifyCurrentTabVisibility()
diff --git a/Modules/BuildAnalysis/UI/BuildAnalysisWindow.cs b/Modules/BuildAnalysis/UI/BuildAnalysisWindow.cs
index 6ddb06bb57..ca70297e84 100644
--- a/Modules/BuildAnalysis/UI/BuildAnalysisWindow.cs
+++ b/Modules/BuildAnalysis/UI/BuildAnalysisWindow.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
+using System.Threading.Tasks;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
@@ -19,12 +20,11 @@ internal interface IBuildListActions
void RegenerateAnalysis(BuildEntry build);
}
- [EditorWindowTitle(title = "Build Analysis", icon = "UnityEditor.ProfilerWindow")]
+ [EditorWindowTitle(title = "Build Analysis", icon = "BuildAnalysisWindow")]
internal class BuildAnalysisWindow : EditorWindow, IBuildListActions
{
private const string k_OpenWindowCommand = "ContentBuild/OpenBuildAnalysisWindow";
- private const string k_WindowTitle = "Build Analysis";
private const string k_UxmlPath = "BuildAnalysis/UXML/BuildAnalysisWindow.uxml";
private const string k_UssPath = "BuildAnalysis/StyleSheets/BuildAnalysisWindow.uss";
private const string k_UssClassDark = "build-analysis-window--dark";
@@ -35,17 +35,28 @@ internal class BuildAnalysisWindow : EditorWindow, IBuildListActions
private const string k_SplitterKey = k_KeyPrefix + "SplitterPosition";
private const string k_InspectorOpenKey = k_KeyPrefix + "InspectorPanelOpen";
+ private const string k_InspectorToggleTooltip = "Toggle Inspector";
+ private const string k_InspectorToggleDisabledTooltip = "The inspector is only available on the Assets tab";
+
+ private const string k_HelpButtonTooltip = "Open Build Analysis documentation";
+ // Manual topic slug (Documentation/ManualDocs/md/build-analysis-window-reference.md).
+ private const string k_DocumentationPage = "build-analysis-window-reference";
+
+ private const string k_LoadingMessage = "Analyzing build…";
+
private TwoPaneSplitView m_SplitView;
private ToolbarToggle m_InspectorToggle;
private TabView m_TabView;
private Tab m_OverviewTab;
private Tab m_AssetsTab;
+ private LoadingOverlay m_LoadingOverlay;
private BuildListPanel m_BuildListPanel;
private BuildAnalysisService m_Service;
private BuildAnalysisTabHost m_TabHost;
private BuildHistoryWatcher m_Watcher;
- private BuildAnalysis m_SelectedBuildAnalysis;
+
+ private SelectionGate m_Gate;
[MenuItem("Window/Analysis/Build Analysis")]
internal static void ShowWindow()
@@ -57,7 +68,6 @@ internal static void ShowWindow()
}
var window = GetWindow(false);
- window.titleContent = new GUIContent(k_WindowTitle);
window.minSize = new Vector2(750, 400);
}
@@ -67,21 +77,34 @@ private void OnEnable()
var fileSystem = new BuildAnalysisFileSystem();
var enumerator = new BuildEnumerator(buildHistory);
- var analyzer = new BuildAnalyzer(new BuildReportConverter(), fileSystem, buildHistory);
- m_Service = new BuildAnalysisService(enumerator, analyzer, fileSystem, new BuildAnalysisProgressReporter(), buildHistory);
+ var converter = new BuildReportConverter();
+ var assetResolver = new SourceBuildAssetResolver(buildHistory, converter);
+ var analyzer = new BuildAnalyzer(converter, fileSystem, buildHistory, assetResolver);
+ m_Service = new BuildAnalysisService(enumerator, analyzer, fileSystem, buildHistory);
m_Watcher = new BuildHistoryWatcher(buildHistory);
m_Watcher.BuildHistoryChanged += RefreshBuildList;
m_Watcher.Enable();
+
+ AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
}
private void OnDisable()
{
+ AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
+ m_Service?.Dispose();
m_Watcher.Disable();
m_Watcher.BuildHistoryChanged -= RefreshBuildList;
SavePersistedState();
}
+ // Cancel in-flight analysis before the domain is torn down so continuations don't run against a
+ // half-dead window. OnEnable rebuilds a fresh service (and cancellation token) after the reload.
+ private void OnBeforeAssemblyReload()
+ {
+ m_Service?.CancelPending();
+ }
+
public void CreateGUI()
{
var visualTree = EditorGUIUtility.LoadRequired(k_UxmlPath) as VisualTreeAsset;
@@ -101,6 +124,8 @@ public void CreateGUI()
m_OverviewTab = rootVisualElement.Q("overview-tab");
m_AssetsTab = rootVisualElement.Q("assets-tab");
+ m_Gate = new SelectionGate(() => rootVisualElement?.panel != null);
+
var buildListHost = rootVisualElement.Q("build-list-host");
m_BuildListPanel = new BuildListPanel(this);
m_BuildListPanel.SelectionChanged += OnBuildSelectionChanged;
@@ -121,10 +146,12 @@ private void SetupInspectorToggle()
if (tabViewport == null)
throw new InvalidOperationException($"{BuildAnalysisConstants.k_ConsoleLogPrefix} TabView content viewport .unity-tab-view__content-viewport not found.");
+ SetupHelpButton(tabViewport);
+
m_InspectorToggle = new ToolbarToggle
{
name = "inspector-toggle",
- tooltip = "Toggle Inspector",
+ tooltip = k_InspectorToggleTooltip,
};
m_InspectorToggle.AddToClassList("inspector-toggle");
tabViewport.Add(m_InspectorToggle);
@@ -137,6 +164,23 @@ private void SetupInspectorToggle()
});
}
+ private void SetupHelpButton(VisualElement tabViewport)
+ {
+ var helpButton = new ToolbarButton(() => Help.BrowseURL(GetDocumentationUrl()))
+ {
+ name = "help-button",
+ tooltip = k_HelpButtonTooltip,
+ };
+ helpButton.AddToClassList("help-button");
+ tabViewport.Add(helpButton);
+ }
+
+ private static string GetDocumentationUrl()
+ {
+ var version = UnityEditorInternal.InternalEditorUtility.GetUnityVersion();
+ return $"https://docs.unity3d.com/{version.Major}.{version.Minor}/Documentation/Manual/{k_DocumentationPage}.html";
+ }
+
private void SetupTabs()
{
m_TabHost = new BuildAnalysisTabHost(m_TabView);
@@ -146,11 +190,27 @@ private void SetupTabs()
assetsTabView.InspectorOpenRequested += () => m_InspectorToggle.value = true;
m_TabHost.Register(m_AssetsTab, assetsTabView);
+ // Only the Assets tab has an inspector; disable the toggle on tabs that don't.
+ m_TabView.activeTabChanged += (_, activeTab) => UpdateInspectorToggleEnabled(activeTab);
+ UpdateInspectorToggleEnabled(m_TabView.activeTab);
+
+ m_LoadingOverlay = new LoadingOverlay();
+ m_TabView.contentContainer.Add(m_LoadingOverlay);
+
m_TabHost.NotifyCurrentTabVisibility();
m_TabHost.SetInspectorOpen(m_InspectorToggle.value);
m_TabHost.SetSelection(null, null);
}
+ private void UpdateInspectorToggleEnabled(Tab activeTab)
+ {
+ var supportsInspector = activeTab == m_AssetsTab;
+ m_InspectorToggle.SetEnabled(supportsInspector);
+ m_InspectorToggle.tooltip = supportsInspector
+ ? k_InspectorToggleTooltip
+ : k_InspectorToggleDisabledTooltip;
+ }
+
private static void ApplyThemeClass(VisualElement view)
{
view.RemoveFromClassList(k_UssClassDark);
@@ -163,21 +223,75 @@ private void RefreshBuildList()
m_BuildListPanel.SetBuilds(m_Service.GetBuilds(), BuildHistory.BuildHistoryLimit);
}
- private void OnBuildSelectionChanged(BuildEntry selection)
+ private async void OnBuildSelectionChanged(BuildEntry selection)
{
if (selection == null)
{
- m_SelectedBuildAnalysis = null;
+ // Invalidate any in-flight load (so its continuation is dropped as stale) and clear the view.
+ m_Gate.Clear();
m_TabHost.SetSelection(null, null);
+ m_LoadingOverlay.Hide();
+ return;
+ }
+
+ // Already loading or showing this build (e.g. a build-list refresh re-fired selection) — skip.
+ if (m_Gate.IsCurrentTarget(selection.BuildSessionGUID))
+ return;
+
+ await LoadAndApplyAsync(selection, () => m_Service.GetBuildAnalysisAsync(selection.BuildSessionGUID));
+ }
+
+ // Apply for both selection and regenerate: show the overlay, await the result, and
+ // update the tabs only if this is still the latest request and the window is still alive.
+ private async Task LoadAndApplyAsync(BuildEntry build, Func> load)
+ {
+ var seq = m_Gate.Begin(build.BuildSessionGUID);
+
+ // Show the loading overlay over the active tab's content before we await.
+ m_LoadingOverlay.Show(k_LoadingMessage);
+
+ BuildAnalysis analysis;
+ try
+ {
+ analysis = await load();
+ }
+ catch (OperationCanceledException)
+ {
+ // Service was torn down / reloaded mid-load (Dispose or CancelPending cancelled the token
+ // before the off-thread work started). Swallow so the async-void caller doesn't log it; the
+ // window is gone or being rebuilt, so there's nothing to update.
+ return;
+ }
+ catch (Exception e)
+ {
+ if (!m_Gate.IsStale(seq))
+ {
+ m_TabHost.SetSelection(null, null); // clear to no-selection
+ m_LoadingOverlay.Hide();
+ m_Gate.Clear(); // a failed load isn't shown — drop the target so re-selecting retries
+ Debug.LogError($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to analyze build: {e.Message}");
+ }
return;
}
- m_SelectedBuildAnalysis = m_Service.GetBuildAnalysis(selection.BuildSessionGUID);
- m_TabHost.SetSelection(selection, m_SelectedBuildAnalysis);
+ if (m_Gate.IsStale(seq))
+ return;
+
+ m_TabHost.SetSelection(build, analysis);
+ m_LoadingOverlay.Hide();
+
+ // A build that produced no analysis isn't shown — drop the target so re-selecting retries.
+ if (analysis == null)
+ m_Gate.Clear();
}
private void SavePersistedState()
{
+ // OnEnable/OnDisable can run without CreateGUI in between: on unmaximize Unity
+ // deserializes the maximize backup window (firing OnEnable) and immediately destroys
+ // it (firing OnDisable), but never shows it, so CreateGUI never assigns m_SplitView.
+ if (m_SplitView == null)
+ return;
EditorPrefs.SetFloat(k_SplitterKey, m_SplitView.fixedPaneInitialDimension);
}
@@ -189,8 +303,8 @@ void IBuildListActions.DeleteBuild(BuildEntry build)
return;
var confirm = EditorUtility.DisplayDialog(
- "Delete Build",
- $"Delete build data for '{build.BuildName}'?\nThis cannot be undone.",
+ "Delete Build Report Directory",
+ $"Delete Build Report Directory from {BuildHistory.BuildHistoryDirectory}?\n\nThis does not delete asset database artifacts nor build outputs. This cannot be undone.",
"Delete",
"Cancel");
if (!confirm)
@@ -218,10 +332,10 @@ void IBuildListActions.DeleteAllBuilds()
if (builds.Length == 0)
return;
- var buildNoun = builds.Length == 1 ? "build" : "builds";
+ var directoryNoun = builds.Length == 1 ? "Build Report Directory" : "Build Report Directories";
var confirm = EditorUtility.DisplayDialog(
- "Delete All Builds",
- $"Delete all {builds.Length} {buildNoun} from {BuildHistory.BuildHistoryDirectory}?\nThis cannot be undone.",
+ "Delete All Build Report Directories",
+ $"Delete all {builds.Length} {directoryNoun} from {BuildHistory.BuildHistoryDirectory}?\n\nThis does not delete asset database artifacts nor build outputs. This cannot be undone.",
"Delete All",
"Cancel");
if (!confirm)
@@ -258,20 +372,37 @@ void IBuildListActions.CopyPath(BuildEntry build)
EditorGUIUtility.systemCopyBuffer = build.FolderPath;
}
- void IBuildListActions.RegenerateAnalysis(BuildEntry build)
+ async void IBuildListActions.RegenerateAnalysis(BuildEntry build)
{
if (build == null)
return;
+ await LoadAndApplyAsync(build, () => m_Service.RegenerateBuildAnalysisAsync(build.BuildSessionGUID));
+ }
+ }
- try
- {
- m_SelectedBuildAnalysis = m_Service.RegenerateBuildAnalysis(build.BuildSessionGUID);
- m_TabHost.SetSelection(build, m_SelectedBuildAnalysis);
- }
- catch (Exception e)
- {
- Debug.LogError($"{BuildAnalysisConstants.k_ConsoleLogPrefix} Failed to re-generate analysis: {e.Message}");
- }
+ // Decides which async selection/regenerate result is still worth applying.
+ internal sealed class SelectionGate
+ {
+ private readonly Func m_IsAlive;
+ private int m_Seq;
+ private GUID m_TargetGuid;
+
+ public SelectionGate(Func isAlive) => m_IsAlive = isAlive;
+
+ public int Begin(GUID target)
+ {
+ m_TargetGuid = target;
+ return ++m_Seq;
+ }
+
+ public bool IsStale(int seq) => seq != m_Seq || !m_IsAlive();
+
+ public bool IsCurrentTarget(GUID guid) => guid == m_TargetGuid;
+
+ public void Clear()
+ {
+ m_Seq++;
+ m_TargetGuid = default;
}
}
}
diff --git a/Modules/BuildAnalysis/UI/BuildListPanel.cs b/Modules/BuildAnalysis/UI/BuildListPanel.cs
index 5aa486705d..a2ebea6b96 100644
--- a/Modules/BuildAnalysis/UI/BuildListPanel.cs
+++ b/Modules/BuildAnalysis/UI/BuildListPanel.cs
@@ -18,8 +18,9 @@ internal sealed class BuildListPanel : VisualElement
internal const string k_FooterWarningClass = "build-list-footer--warning";
private const double k_FooterWarnThreshold = 0.8;
- private const string k_FooterWarningTooltip = "Build limit approaching. To edit the limit, go to Project Settings > Build Pipeline.";
- private const string k_FooterExceededTooltip = "Build limit exceeded. Older builds will be removed on the next build. To edit the limit, go to Project Settings > Build Pipeline.";
+ internal const string k_FooterDefaultTooltip = "To edit the build limit, go to Project Settings > Build Pipeline.";
+ private const string k_FooterWarningTooltip = "Build limit approaching. Older builds will be automatically deleted once the limit is hit. To edit the limit, go to Project Settings > Build Pipeline.";
+ private const string k_FooterExceededTooltip = "Build limit exceeded. Older builds will be automatically deleted on the next build. To edit the limit, go to Project Settings > Build Pipeline.";
public event Action SelectionChanged;
@@ -28,7 +29,6 @@ internal sealed class BuildListPanel : VisualElement
private readonly ToolbarMenu m_SettingsMenu;
private readonly ListView m_BuildListView;
private readonly VisualElement m_EmptyState;
- private readonly Label m_EmptyStateTitle;
private readonly Label m_EmptyStateDescription;
private readonly VisualElement m_FooterRoot;
private readonly Label m_FooterLabel;
@@ -52,7 +52,6 @@ public BuildListPanel(IBuildListActions actions)
m_SettingsMenu = this.Q("build-settings-menu");
m_BuildListView = this.Q("build-list");
m_EmptyState = this.Q("empty-state");
- m_EmptyStateTitle = this.Q("empty-state-title");
m_EmptyStateDescription = this.Q("empty-state-description");
m_FooterRoot = this.Q("build-list-footer");
m_FooterLabel = this.Q("build-list-footer__label");
@@ -110,7 +109,7 @@ private void SetupBuildListView()
private void SetupSettingsMenu()
{
- m_SettingsMenu.menu.AppendAction("Delete All Builds...", _ => m_Actions.DeleteAllBuilds());
+ m_SettingsMenu.menu.AppendAction("Delete All...", _ => m_Actions.DeleteAllBuilds());
}
private VisualElement MakeListItem()
@@ -292,13 +291,11 @@ private void UpdateEmptyState()
if (m_AllBuilds.Length == 0)
{
- m_EmptyStateTitle.text = "No builds available";
- m_EmptyStateDescription.text = "Builds will appear here automatically\nafter you build your project.";
+ m_EmptyStateDescription.text = "No Build Reports available.\nReports will be listed here\nafter a player or content build\nhas been created.";
}
else
{
- m_EmptyStateTitle.text = "No matching builds";
- m_EmptyStateDescription.text = $"No builds match '{m_CurrentSearchText}'";
+ m_EmptyStateDescription.text = $"No builds match '{m_CurrentSearchText}'.";
}
}
@@ -308,7 +305,7 @@ private void UpdateFooter(int count, int limit)
{
m_FooterLabel.text = $"{count} builds";
m_FooterRoot.RemoveFromClassList(k_FooterWarningClass);
- m_FooterRoot.tooltip = string.Empty;
+ m_FooterRoot.tooltip = k_FooterDefaultTooltip;
return;
}
@@ -318,7 +315,7 @@ private void UpdateFooter(int count, int limit)
m_FooterRoot.EnableInClassList(k_FooterWarningClass, exceeded || approaching);
m_FooterRoot.tooltip = exceeded ? k_FooterExceededTooltip
: approaching ? k_FooterWarningTooltip
- : string.Empty;
+ : k_FooterDefaultTooltip;
}
internal void PopulateContextMenu(DropdownMenu menu, BuildEntry selection, bool isDeveloperMode)
@@ -335,7 +332,7 @@ internal void PopulateContextMenu(DropdownMenu menu, BuildEntry selection, bool
menu.AppendSeparator();
- menu.AppendAction("Delete", _ => m_Actions.DeleteBuild(selection), status);
+ menu.AppendAction("Delete Build Report Directory", _ => m_Actions.DeleteBuild(selection), status);
}
}
}
diff --git a/Modules/BuildAnalysis/UI/Common/BuildHeaderController.cs b/Modules/BuildAnalysis/UI/Common/BuildHeaderController.cs
index 25c4f76268..b1492aeb97 100644
--- a/Modules/BuildAnalysis/UI/Common/BuildHeaderController.cs
+++ b/Modules/BuildAnalysis/UI/Common/BuildHeaderController.cs
@@ -33,12 +33,10 @@ public BuildHeaderController(VisualElement headerRoot)
m_TimeRange = headerRoot.Q("time-range");
}
- public void Bind(BuildEntry selection, BuildAnalysis analysis)
+ public void Bind(BuildEntry selection)
{
- var summary = analysis.Summary;
-
m_Title.text = selection.BuildName ?? string.Empty;
- m_Subtitle.text = $"{selection.Platform} • {summary.BuildType}";
+ m_Subtitle.text = $"{selection.Platform} • {selection.BuildType}";
m_PlatformIcon.image = IconUtility.GetPlatformIcon(selection.Platform);
m_StatusText.text = selection.BuildResult == BuildResult.Succeeded ? "Success" : "Failure";
m_TimeRange.text = FormatTimeRange(selection.BuildStartedAt, selection.TotalTimeMs);
diff --git a/Modules/BuildAnalysis/UI/Common/LoadingOverlay.cs b/Modules/BuildAnalysis/UI/Common/LoadingOverlay.cs
new file mode 100644
index 0000000000..3d27a040e9
--- /dev/null
+++ b/Modules/BuildAnalysis/UI/Common/LoadingOverlay.cs
@@ -0,0 +1,75 @@
+// Unity C# reference source
+// Copyright (c) Unity Technologies. For terms of use, see
+// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
+
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UnityEditor.Build.Analysis
+{
+ internal sealed class LoadingOverlay : VisualElement
+ {
+ private static readonly string ussClassName = "loading-overlay";
+ private static readonly string spinnerUssClassName = ussClassName + "__spinner";
+ private static readonly string labelUssClassName = ussClassName + "__label";
+
+ private const string k_UssPath = "BuildAnalysis/StyleSheets/LoadingOverlay.uss";
+ private const int k_FrameCount = 12;
+ private const long k_FrameIntervalMs = 80; // 12 frames ≈ one rotation per second
+
+ private static Texture[] s_Frames;
+
+ private readonly Image m_Spinner;
+ private readonly Label m_Label;
+ private IVisualElementScheduledItem m_Spin;
+ private int m_Frame;
+
+ public LoadingOverlay()
+ {
+ var styleSheet = EditorGUIUtility.LoadRequired(k_UssPath) as StyleSheet;
+ styleSheets.Add(styleSheet);
+ AddToClassList(ussClassName);
+
+ pickingMode = PickingMode.Position; // block clicks to the content beneath while shown
+ style.display = DisplayStyle.None;
+
+ s_Frames ??= LoadFrames();
+
+ m_Spinner = new Image { image = s_Frames[0] };
+ m_Spinner.AddToClassList(spinnerUssClassName);
+ Add(m_Spinner);
+
+ m_Label = new Label();
+ m_Label.AddToClassList(labelUssClassName);
+ Add(m_Label);
+ }
+
+ public void Show(string message)
+ {
+ m_Label.text = message ?? string.Empty;
+ style.display = DisplayStyle.Flex;
+ m_Spin ??= schedule.Execute(Advance).Every(k_FrameIntervalMs);
+ m_Spin.Resume();
+ }
+
+ public void Hide()
+ {
+ style.display = DisplayStyle.None;
+ m_Spin?.Pause();
+ }
+
+ private void Advance()
+ {
+ m_Frame = (m_Frame + 1) % s_Frames.Length;
+ m_Spinner.image = s_Frames[m_Frame];
+ }
+
+ private static Texture[] LoadFrames()
+ {
+ var frames = new Texture[k_FrameCount];
+ for (var i = 0; i < frames.Length; i++)
+ frames[i] = EditorGUIUtility.IconContent("WaitSpin" + i.ToString("00")).image;
+ return frames;
+ }
+ }
+}
diff --git a/Modules/BuildAnalysis/UI/Common/ZebraEmptyBody.cs b/Modules/BuildAnalysis/UI/Common/ZebraEmptyBody.cs
index c791664922..840858d0ad 100644
--- a/Modules/BuildAnalysis/UI/Common/ZebraEmptyBody.cs
+++ b/Modules/BuildAnalysis/UI/Common/ZebraEmptyBody.cs
@@ -8,13 +8,14 @@
namespace UnityEditor.Build.Analysis
{
- // Drop-in overlay that continues a MultiColumnListView's alternating-row "zebra" pattern
+ // Drop-in overlay that continues a vertical collection view's alternating-row "zebra" pattern
// past the last visible row, filling any leftover body space. Reads body geometry and row
- // height live from the MCLV so it works equally for fixed-height tables and ones hosted
- // inside a TwoPaneSplitView (where the body height is user-driven).
+ // height live from the list so it works equally for fixed-height tables and ones hosted
+ // inside a TwoPaneSplitView (where the body height is user-driven). Works with any
+ // BaseVerticalCollectionView (MultiColumnListView, ListView, ...).
//
// m_EmptyBody = new ZebraEmptyBody(m_ListView);
- // hostContainer.Add(m_EmptyBody); // any ancestor that visually overlaps the MCLV body
+ // hostContainer.Add(m_EmptyBody); // any ancestor that visually overlaps the list body
// // ...then after every m_ListView.RefreshItems():
// m_EmptyBody.Refresh();
internal sealed class ZebraEmptyBody : VisualElement
@@ -24,12 +25,13 @@ internal sealed class ZebraEmptyBody : VisualElement
public static readonly string stripeAltUssClassName = stripeUssClassName + "--alt";
// Public USS class name is part of MCLV's stable surface; the C# header type is internal.
+ // A headerless list (e.g. a plain ListView) simply has no element matching this class.
private const string k_HeaderClassName = "unity-multi-column-header";
private const string k_UssPath = "BuildAnalysis/StyleSheets/ZebraEmptyBody.uss";
- private readonly MultiColumnListView m_ListView;
+ private readonly BaseVerticalCollectionView m_ListView;
- internal ZebraEmptyBody(MultiColumnListView listView)
+ internal ZebraEmptyBody(BaseVerticalCollectionView listView)
{
m_ListView = listView;
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetActions.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetActions.cs
index a5f087d902..e94cb635eb 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetActions.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetActions.cs
@@ -9,25 +9,37 @@ namespace UnityEditor.Build.Analysis
{
///
/// Shared entry points for asset-row actions
- /// The two action implementations are exposed as swappable delegates so unit tests can capture
+ /// The action implementations are exposed as swappable delegates so unit tests can capture
/// invocations without reaching into the Editor APIs.
///
internal static class AssetActions
{
+ // User-facing message when a build-report asset can no longer be resolved in the project.
+ internal const string k_MissingAssetMessage =
+ "Asset not found. It may have been renamed, moved, or deleted since this build was analyzed.";
+
internal static Action ShowInProjectImpl = DefaultShowInProject;
internal static Action CopyPathImpl = DefaultCopyPath;
+ internal static Func CanShowInProjectImpl = DefaultCanShowInProject;
+ internal static Action NotifyMissingImpl = DefaultNotifyMissing;
public static void ShowInProject(string assetPath) => ShowInProjectImpl(assetPath);
public static void CopyPath(string assetPath) => CopyPathImpl(assetPath);
+ // Whether "Show in Project" can resolve the asset; used to disable the menu item up front.
+ public static bool CanShowInProject(string assetPath) => CanShowInProjectImpl(assetPath);
+
+ private static bool DefaultCanShowInProject(string assetPath)
+ => !string.IsNullOrEmpty(assetPath) && AssetDatabase.LoadMainAssetAtPath(assetPath) != null;
+
private static void DefaultShowInProject(string assetPath)
{
- if (string.IsNullOrEmpty(assetPath))
- return;
-
- var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
+ var obj = string.IsNullOrEmpty(assetPath) ? null : AssetDatabase.LoadMainAssetAtPath(assetPath);
if (obj == null)
+ {
+ NotifyMissingImpl(assetPath);
return;
+ }
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
@@ -37,5 +49,19 @@ private static void DefaultCopyPath(string assetPath)
{
EditorGUIUtility.systemCopyBuffer = assetPath ?? string.Empty;
}
+
+ private static void DefaultNotifyMissing(string assetPath)
+ {
+ var windows = Resources.FindObjectsOfTypeAll();
+ if (windows.Length > 0)
+ {
+ var icon = EditorGUIUtility.FindTexture("console.warnicon");
+ windows[0].ShowNotification(new GUIContent(k_MissingAssetMessage, icon));
+ }
+ else
+ {
+ Debug.LogWarning($"{BuildAnalysisConstants.k_ConsoleLogPrefix} {k_MissingAssetMessage}");
+ }
+ }
}
}
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetInspector.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetInspector.cs
index b818386bfd..8ca55f67dc 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetInspector.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetInspector.cs
@@ -3,7 +3,9 @@
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
+using System.Collections.Generic;
using System.IO;
+using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
@@ -11,92 +13,471 @@ namespace UnityEditor.Build.Analysis
{
///
/// Right-docked side panel that shows properties of the asset currently selected in the
- /// Asset Table.
+ /// Asset Table or Root Asset Table.
///
- internal class AssetInspector : VisualElement
+ internal sealed class AssetInspector : VisualElement
{
- private const string k_UxmlPath = "BuildAnalysis/UXML/AssetInspector.uxml";
- private const string k_EmptyClass = "asset-inspector--empty";
+ private const string k_UssPath = "BuildAnalysis/StyleSheets/AssetInspector.uss";
- private readonly Image m_HeaderIcon;
- private readonly Label m_HeaderName;
- private readonly Button m_SelectButton;
- private readonly Label m_OutputSizeValue;
- private readonly Label m_ImporterTypeValue;
- private readonly Label m_ObjectsCountValue;
- private readonly Label m_ResourcesFilesValue;
- private readonly Label m_AssetPathValue;
- private readonly VisualElement m_Root;
+ internal enum Mode { Empty, Asset, Root }
- private string m_CurrentPath;
+ private readonly InspectorHeader m_Header = new InspectorHeader();
+ private readonly AssetInspectorBody m_AssetBody = new AssetInspectorBody();
+ private readonly RootAssetInspectorBody m_RootBody = new RootAssetInspectorBody();
+
+ internal Mode CurrentMode { get; private set; }
public AssetInspector()
{
- var template = EditorGUIUtility.LoadRequired(k_UxmlPath) as VisualTreeAsset;
- template.CloneTree(this);
+ AddToClassList("inspector");
+
+ styleSheets.Add(EditorGUIUtility.LoadRequired(k_UssPath) as StyleSheet);
+
+ Add(m_Header);
+ Add(m_AssetBody);
+ Add(m_RootBody);
+
+ ShowEmpty();
+ }
+
+ public void ShowAsset(BuildAnalysisAsset asset, BuildAnalysisImporterType? importer)
+ {
+ var path = asset.Path ?? string.Empty;
+ m_Header.Bind(IconUtility.GetAssetIcon(path), Path.GetFileNameWithoutExtension(path), path);
+ m_AssetBody.Bind(asset, importer);
+ SetMode(Mode.Asset);
+ }
+
+ public void ShowRootAsset(BuildAnalysisRootAsset root, BuildAnalysisAsset rootAsset,
+ BuildAnalysisAsset[] assets)
+ {
+ var path = rootAsset.Path ?? string.Empty;
+ // Root names keep the extension to match the Root Asset Table's Name column.
+ var title = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileName(path);
+ m_Header.Bind(IconUtility.GetAssetIcon(path), title, path);
+ m_RootBody.Bind(root, rootAsset, assets);
+ SetMode(Mode.Root);
+ }
+
+ public void ShowEmpty() => SetMode(Mode.Empty);
+
+ private void SetMode(Mode mode)
+ {
+ CurrentMode = mode;
+ m_Header.style.display = mode == Mode.Empty ? DisplayStyle.None : DisplayStyle.Flex;
+ m_AssetBody.style.display = mode == Mode.Asset ? DisplayStyle.Flex : DisplayStyle.None;
+ m_RootBody.style.display = mode == Mode.Root ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+ }
+
+ /// Asset icon + filename + a right-aligned Select button (= Show in Project).
+ internal sealed class InspectorHeader : VisualElement
+ {
+ private readonly Image m_Icon;
+ private readonly Label m_Name;
+ private readonly Button m_Select;
+ private string m_Path;
+
+ public InspectorHeader()
+ {
+ AddToClassList("inspector__header");
+
+ m_Icon = new Image { scaleMode = ScaleMode.ScaleToFit };
+ m_Icon.AddToClassList("inspector__header-icon");
+ Add(m_Icon);
+
+ m_Name = new Label();
+ m_Name.AddToClassList("inspector__header-name");
+ Add(m_Name);
+
+ m_Select = new Button(OnSelectClicked) { text = "Select" };
+ m_Select.AddToClassList("inspector__select-button");
+ Add(m_Select);
+ }
+
+ internal string Title => m_Name.text;
+
+ public void Bind(Texture icon, string title, string assetPath)
+ {
+ m_Icon.image = icon;
+ m_Name.text = title;
+ m_Path = assetPath;
+ // Grey out Select when the asset can no longer be resolved
+ var canSelect = AssetActions.CanShowInProject(assetPath);
+ m_Select.SetEnabled(canSelect);
+ m_Select.tooltip = canSelect
+ ? "Select this asset in the Project window"
+ : AssetActions.k_MissingAssetMessage;
+ }
+
+ private void OnSelectClicked() => AssetActions.ShowInProject(m_Path);
+ }
+
+ /// One label/value row: label left, value right-aligned, selectable, ellipsis on overflow.
+ internal sealed class InspectorField : VisualElement
+ {
+ private readonly Label m_Value;
+
+ public InspectorField(string label)
+ {
+ AddToClassList("inspector__field");
+
+ var labelEl = new Label(label);
+ labelEl.AddToClassList("inspector__field-label");
+ Add(labelEl);
+
+ m_Value = new Label();
+ m_Value.AddToClassList("inspector__field-value");
+ m_Value.selection.isSelectable = true;
+ Add(m_Value);
+ }
+
+ internal string Value { get => m_Value.text; set => m_Value.text = value; }
+ internal string ValueTooltip { get => m_Value.tooltip; set => m_Value.tooltip = value; }
+ internal bool ValueSelectable => m_Value.selection.isSelectable;
+ }
+
+ /// Body for an Asset Table selection: the asset's own properties.
+ internal sealed class AssetInspectorBody : VisualElement
+ {
+ internal readonly InspectorField OutputSize = new InspectorField("Output size");
+ internal readonly InspectorField ImporterType = new InspectorField("Importer type");
+ internal readonly InspectorField ObjectsCount = new InspectorField("Objects count");
+ internal readonly InspectorField ResourcesFiles = new InspectorField("Resources files");
+ internal readonly InspectorField AssetPath = new InspectorField("Asset path");
+
+ public AssetInspectorBody()
+ {
+ AddToClassList("inspector__body");
+ Add(OutputSize);
+ Add(ImporterType);
+ Add(ObjectsCount);
+ Add(ResourcesFiles);
+ Add(AssetPath);
+ }
+
+ public void Bind(BuildAnalysisAsset asset, BuildAnalysisImporterType? importer)
+ {
+ var path = asset.Path ?? string.Empty;
+ OutputSize.Value = FormatUtility.FormatSize(asset.OutputSizeBytes);
+ ImporterType.Value = importer?.Name ?? string.Empty;
+ ObjectsCount.Value = asset.ObjectCount.ToString();
+ ResourcesFiles.Value = asset.ResourceCount.ToString();
+ AssetPath.Value = path;
+ AssetPath.ValueTooltip = path;
+ }
+ }
+
+ /// Body for a Root Asset Table selection: reachability stats + a "References" list.
+ internal sealed class RootAssetInspectorBody : VisualElement
+ {
+ internal readonly InspectorField DirectSize = new InspectorField("Direct size");
+ internal readonly InspectorField DirectAssets = new InspectorField("Direct assets");
+ internal readonly InspectorField TotalSize = new InspectorField("Total size");
+ internal readonly InspectorField TotalAssets = new InspectorField("Total assets");
+ internal readonly InspectorField OutputSize = new InspectorField("Output size");
+ internal readonly InspectorField AssetPath = new InspectorField("Asset path");
+ internal readonly ReferencesView References = new ReferencesView();
+
+ public RootAssetInspectorBody()
+ {
+ AddToClassList("inspector__body");
+ Add(DirectSize);
+ Add(DirectAssets);
+ Add(TotalSize);
+ Add(TotalAssets);
+ Add(OutputSize);
+ Add(AssetPath);
+ Add(References);
+ }
+
+ public void Bind(BuildAnalysisRootAsset root, BuildAnalysisAsset rootAsset,
+ BuildAnalysisAsset[] assets)
+ {
+ var path = rootAsset.Path ?? string.Empty;
+ DirectSize.Value = FormatUtility.FormatSize(root.DirectSizeBytes);
+ DirectAssets.Value = FormatUtility.FormatCount(root.DirectAssetCount);
+ TotalSize.Value = FormatUtility.FormatSize(root.TotalSizeBytes);
+ TotalAssets.Value = FormatUtility.FormatCount(root.TotalAssetCount);
+ OutputSize.Value = FormatUtility.FormatSize(rootAsset.OutputSizeBytes);
+ AssetPath.Value = path;
+ AssetPath.ValueTooltip = path;
+ References.Bind(root.ReferencedAssetIds, assets);
+ }
+ }
+
+ ///
+ /// Read-only "References" list of all assets a root pulls in (its full reachable set),
+ /// sorted by size descending.
+ /// Rows are precomputed in so bindItem stays allocation-free.
+ ///
+ internal sealed class ReferencesView : VisualElement
+ {
+ private const string k_AllExtensions = "All";
+ private const int k_NoExtensionId = -1;
+
+ private struct Row
+ {
+ public string Name;
+ public string Path;
+ public string Size;
+ public ulong SizeBytes;
+ public Texture Icon;
+ public int ExtensionId;
+ }
+
+ private readonly Label m_Header;
+ private readonly DropdownField m_ViewDropdown;
+ private readonly ToolbarSearchField m_SearchField;
+ private readonly ListView m_List;
+ private readonly ZebraEmptyBody m_EmptyBody;
+ private readonly Label m_FooterCount;
+
+ private readonly List m_Rows = new List();
+ // Indices into m_Rows (kept in m_Rows' size-sorted order) that pass the active filters.
+ private readonly List m_FilteredIndices = new List();
+ private string[] m_ExtensionPool = Array.Empty();
+
+ private IVisualElementScheduledItem m_SearchDebounce;
+ private int m_ViewFilterId = k_NoExtensionId;
+ private string m_SearchText = string.Empty;
+
+ public ReferencesView()
+ {
+ AddToClassList("inspector__references");
+
+ m_Header = new Label("References");
+ m_Header.AddToClassList("inspector__references-header");
+ Add(m_Header);
- m_Root = this.Q("asset-inspector");
- m_HeaderIcon = this.Q("header-icon");
- m_HeaderName = this.Q("header-name");
- m_SelectButton = this.Q("select-button");
- m_OutputSizeValue = this.Q("output-size-value");
- m_ImporterTypeValue = this.Q("importer-type-value");
- m_ObjectsCountValue = this.Q("objects-count-value");
- m_ResourcesFilesValue = this.Q("resources-files-value");
- m_AssetPathValue = this.Q("asset-path-value");
+ var toolbar = new VisualElement();
+ toolbar.AddToClassList("data-list__toolbar");
+ toolbar.AddToClassList("inspector__references-toolbar");
- m_HeaderIcon.scaleMode = ScaleMode.ScaleToFit;
+ m_ViewDropdown = new DropdownField { label = "View:", tooltip = "Filter by file extension", name = "view-dropdown" };
+ m_ViewDropdown.AddToClassList("data-list__toolbar-control");
+ m_ViewDropdown.AddToClassList("data-list__toolbar-popup");
+ m_ViewDropdown.choices = new List { k_AllExtensions };
+ m_ViewDropdown.SetValueWithoutNotify(k_AllExtensions);
+ toolbar.Add(m_ViewDropdown);
- m_SelectButton.clicked += OnSelectClicked;
+ var spacer = new VisualElement();
+ spacer.AddToClassList("data-list__toolbar-spacer");
+ toolbar.Add(spacer);
+
+ m_SearchField = new ToolbarSearchField { name = "search-field", tooltip = "Search asset name" };
+ m_SearchField.AddToClassList("data-list__toolbar-search");
+ toolbar.Add(m_SearchField);
+ Add(toolbar);
+
+ m_List = new ListView
+ {
+ itemsSource = m_FilteredIndices,
+ fixedItemHeight = 20,
+ selectionType = SelectionType.None,
+ showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
+ makeItem = MakeItem,
+ bindItem = BindItem,
+ };
+ m_List.AddToClassList("inspector__references-list");
+ Add(m_List);
+
+ var footer = new VisualElement();
+ footer.AddToClassList("data-list__footer");
+ footer.AddToClassList("inspector__references-footer");
+ m_FooterCount = new Label { name = "footer-count-label" };
+ m_FooterCount.AddToClassList("data-list__footer-count");
+ footer.Add(m_FooterCount);
+ Add(footer);
+
+ // Continue the zebra pattern past the last row into the empty body, matching the tables.
+ m_EmptyBody = new ZebraEmptyBody(m_List);
+ Add(m_EmptyBody);
+
+ m_ViewDropdown.RegisterValueChangedCallback(evt => SetViewFilter(evt.newValue));
+ m_SearchField.RegisterValueChangedCallback(evt =>
+ {
+ var newText = evt.newValue ?? string.Empty;
+ m_SearchDebounce?.Pause();
+ m_SearchDebounce = schedule.Execute(() => SetSearchText(newText)).StartingIn(200);
+ });
}
- ///
- /// Populate the inspector for the given asset, or clear it if is null.
- /// is the resolved importer for the asset; pass null when
- /// the asset's ImporterTypeId doesn't resolve to a known type, and the Importer Type
- /// field renders empty.
- ///
- public void SetAsset(BuildAnalysisAsset? asset, BuildAnalysisImporterType? importerType)
+ // Visible (post-filter) row count.
+ internal int Count => m_FilteredIndices.Count;
+ internal string HeaderText => m_Header.text;
+
+ public void Bind(int[] referencedAssetIds, BuildAnalysisAsset[] assets)
+ {
+ BuildRows(referencedAssetIds, assets);
+ BuildViewDropdownChoices();
+ ResetFilterState();
+ ApplyFilters();
+ }
+
+ private void BuildRows(int[] referencedAssetIds, BuildAnalysisAsset[] assets)
{
- if (!asset.HasValue)
+ m_Rows.Clear();
+
+ // Build a deduplicated extension pool while creating rows (insertion order first), then
+ // sort the pool alphabetically and remap each row's id to its sorted position.
+ var insertionMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var insertionPool = new List();
+
+ if (referencedAssetIds != null && assets != null)
{
- Reset();
- return;
+ foreach (var id in referencedAssetIds)
+ {
+ if (id < 0 || id >= assets.Length)
+ continue;
+ var a = assets[id];
+ var p = a.Path ?? string.Empty;
+
+ var ext = Path.GetExtension(p);
+ int extId;
+ if (string.IsNullOrEmpty(ext))
+ {
+ extId = k_NoExtensionId;
+ }
+ else if (!insertionMap.TryGetValue(ext, out extId))
+ {
+ extId = insertionPool.Count;
+ var lowerExt = ext.ToLowerInvariant();
+ insertionPool.Add(lowerExt);
+ insertionMap[lowerExt] = extId;
+ }
+
+ m_Rows.Add(new Row
+ {
+ Name = string.IsNullOrEmpty(p) ? string.Empty : Path.GetFileName(p),
+ Path = p,
+ Size = FormatUtility.FormatSize(a.OutputSizeBytes),
+ SizeBytes = a.OutputSizeBytes,
+ Icon = IconUtility.GetAssetIcon(p),
+ ExtensionId = extId,
+ });
+ }
}
- var a = asset.Value;
- m_Root.RemoveFromClassList(k_EmptyClass);
+ var sortedPool = new List(insertionPool);
+ sortedPool.Sort(StringComparer.Ordinal);
+ if (insertionPool.Count > 0)
+ {
+ var remap = new int[insertionPool.Count];
+ for (int sortedId = 0; sortedId < sortedPool.Count; sortedId++)
+ remap[insertionMap[sortedPool[sortedId]]] = sortedId;
+ for (int i = 0; i < m_Rows.Count; i++)
+ {
+ var row = m_Rows[i];
+ if (row.ExtensionId != k_NoExtensionId)
+ {
+ row.ExtensionId = remap[row.ExtensionId];
+ m_Rows[i] = row;
+ }
+ }
+ }
+ m_ExtensionPool = sortedPool.ToArray();
+
+ // Largest contributors first — the reason a user opens this list. Name then path break
+ // ties so equal-size rows keep a deterministic order (List.Sort is unstable).
+ m_Rows.Sort((x, y) =>
+ {
+ var bySize = y.SizeBytes.CompareTo(x.SizeBytes);
+ if (bySize != 0)
+ return bySize;
+ var byName = string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
+ return byName != 0 ? byName : string.Compare(x.Path, y.Path, StringComparison.Ordinal);
+ });
+ }
+
+ private void BuildViewDropdownChoices()
+ {
+ var choices = new List(m_ExtensionPool.Length + 1) { k_AllExtensions };
+ choices.AddRange(m_ExtensionPool);
+ m_ViewDropdown.choices = choices;
+ m_ViewDropdown.SetValueWithoutNotify(k_AllExtensions);
+ }
+
+ private void SetViewFilter(string value)
+ {
+ var newId = string.IsNullOrEmpty(value) || string.Equals(value, k_AllExtensions, StringComparison.Ordinal)
+ ? k_NoExtensionId
+ : Array.IndexOf(m_ExtensionPool, value.ToLowerInvariant());
+
+ if (newId == m_ViewFilterId)
+ return;
+
+ m_ViewFilterId = newId;
+ m_ViewDropdown.SetValueWithoutNotify(newId == k_NoExtensionId ? k_AllExtensions : m_ExtensionPool[newId]);
+ ApplyFilters();
+ }
+
+ private void SetSearchText(string value)
+ {
+ var newText = value ?? string.Empty;
+ if (string.Equals(newText, m_SearchText, StringComparison.Ordinal))
+ return;
+ m_SearchText = newText;
+ ApplyFilters();
+ }
+
+ private void ResetFilterState()
+ {
+ m_SearchDebounce?.Pause();
+ m_ViewFilterId = k_NoExtensionId;
+ m_SearchText = string.Empty;
+ m_SearchField.SetValueWithoutNotify(string.Empty);
+ m_ViewDropdown.SetValueWithoutNotify(k_AllExtensions);
+ }
+
+ private void ApplyFilters()
+ {
+ m_FilteredIndices.Clear();
+ if (m_FilteredIndices.Capacity < m_Rows.Count)
+ m_FilteredIndices.Capacity = m_Rows.Count;
- var path = a.Path ?? string.Empty;
- m_CurrentPath = path;
- m_HeaderIcon.image = IconUtility.GetAssetIcon(path);
- m_HeaderName.text = Path.GetFileNameWithoutExtension(path);
+ var hasViewFilter = m_ViewFilterId != k_NoExtensionId;
+ var hasSearch = !string.IsNullOrEmpty(m_SearchText);
+
+ for (int i = 0; i < m_Rows.Count; i++)
+ {
+ var row = m_Rows[i];
+ if (hasViewFilter && row.ExtensionId != m_ViewFilterId)
+ continue;
+ if (hasSearch && row.Name.IndexOf(m_SearchText, StringComparison.OrdinalIgnoreCase) < 0)
+ continue;
+ m_FilteredIndices.Add(i);
+ }
- m_OutputSizeValue.text = FormatUtility.FormatSize(a.OutputSizeBytes);
- m_ImporterTypeValue.text = importerType?.Name ?? string.Empty;
- m_ObjectsCountValue.text = a.ObjectCount.ToString();
- m_ResourcesFilesValue.text = a.ResourceCount.ToString();
- m_AssetPathValue.text = path;
- m_AssetPathValue.tooltip = path;
+ m_List.RefreshItems();
+ m_FooterCount.text = $"Showing {m_FilteredIndices.Count}/{m_Rows.Count}";
+ m_EmptyBody.Refresh();
}
- private void Reset()
+ private static VisualElement MakeItem()
{
- m_Root.AddToClassList(k_EmptyClass);
- m_CurrentPath = string.Empty;
- m_HeaderIcon.image = null;
- m_HeaderName.text = string.Empty;
- m_OutputSizeValue.text = string.Empty;
- m_ImporterTypeValue.text = string.Empty;
- m_ObjectsCountValue.text = string.Empty;
- m_ResourcesFilesValue.text = string.Empty;
- m_AssetPathValue.text = string.Empty;
- m_AssetPathValue.tooltip = string.Empty;
+ var row = new VisualElement();
+ row.AddToClassList("inspector__references-item");
+ var icon = new Image { scaleMode = ScaleMode.ScaleToFit };
+ icon.AddToClassList("inspector__references-item-icon");
+ row.Add(icon);
+ var name = new Label();
+ name.AddToClassList("inspector__references-item-name");
+ row.Add(name);
+ var size = new Label();
+ size.AddToClassList("inspector__references-item-size");
+ row.Add(size);
+ return row;
}
- private void OnSelectClicked()
+ private void BindItem(VisualElement ve, int index)
{
- AssetActions.ShowInProject(m_CurrentPath);
+ var row = m_Rows[m_FilteredIndices[index]];
+ ((Image)ve.ElementAt(0)).image = row.Icon;
+ var name = (Label)ve.ElementAt(1);
+ name.text = row.Name;
+ name.tooltip = row.Path;
+ ((Label)ve.ElementAt(2)).text = row.Size;
}
}
}
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetRowContextMenu.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetRowContextMenu.cs
index 5dceee9bdc..6742ecb095 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetRowContextMenu.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetRowContextMenu.cs
@@ -32,7 +32,11 @@ internal static void Populate(DropdownMenu menu, string assetPath)
return;
menu.AppendAction("Copy Path", _ => AssetActions.CopyPath(assetPath));
- menu.AppendAction("Show in Project", _ => AssetActions.ShowInProject(assetPath));
+
+ var showStatus = AssetActions.CanShowInProject(assetPath)
+ ? DropdownMenuAction.Status.Normal
+ : DropdownMenuAction.Status.Disabled;
+ menu.AppendAction("Show in Project", _ => AssetActions.ShowInProject(assetPath), showStatus);
}
}
}
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetTable.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetTable.cs
index 1a99c2c46f..61b916444a 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetTable.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetTable.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Unity.Profiling;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
@@ -17,6 +18,8 @@ internal class AssetTable : VisualElement
private const int k_NoExtensionId = -1;
private const string k_UxmlPath = "BuildAnalysis/UXML/AssetTable.uxml";
+ static readonly ProfilerMarker s_BindMarker = new ProfilerMarker("AssetTable.Bind");
+
public event Action SelectionChanged;
private readonly DropdownField m_ViewDropdown;
@@ -105,21 +108,24 @@ private void OnListSelectionChanged(IEnumerable items)
public void Bind(BuildAnalysis analysis)
{
- m_Assets = analysis.Tables.Assets;
- var n = m_Assets.Length;
-
- // Reuse parallel arrays across binds when possible; only grow when capacity is exceeded.
- EnsureCapacity(ref m_Names, n);
- EnsureCapacity(ref m_FormattedSizes, n);
- if (m_ExtensionIds.Length < n)
- m_ExtensionIds = new int[n];
-
- BuildExtensionPoolAndNames(n);
- for (int i = 0; i < n; i++)
- m_FormattedSizes[i] = FormatUtility.FormatSize(m_Assets[i].OutputSizeBytes);
- BuildViewDropdownChoices();
- ResetFilterState();
- ApplyFilters();
+ using (s_BindMarker.Auto())
+ {
+ m_Assets = analysis.Tables.Assets;
+ var n = m_Assets.Length;
+
+ // Reuse parallel arrays across binds when possible; only grow when capacity is exceeded.
+ EnsureCapacity(ref m_Names, n);
+ EnsureCapacity(ref m_FormattedSizes, n);
+ if (m_ExtensionIds.Length < n)
+ m_ExtensionIds = new int[n];
+
+ BuildExtensionPoolAndNames(n);
+ for (int i = 0; i < n; i++)
+ m_FormattedSizes[i] = FormatUtility.FormatSize(m_Assets[i].OutputSizeBytes);
+ BuildViewDropdownChoices();
+ ResetFilterState();
+ ApplyFilters();
+ }
}
private static void EnsureCapacity(ref string[] array, int min)
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetsTabView.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetsTabView.cs
index d30f56f894..ddad09f8bb 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/AssetsTabView.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/AssetsTabView.cs
@@ -29,11 +29,16 @@ internal class AssetsTabView : IBuildAnalysisTabView
private Label m_AssetsValue;
private VisualElement m_RootAssetsCard;
private Label m_RootAssetsValue;
+ private VisualElement m_ContentMain;
+ private HelpBox m_AssetSourceBanner;
+ private HelpBox m_AssetsEmptyState;
private bool m_HasLaidOut;
private bool m_InspectorOpen;
+ private bool m_SuppressSelectionClear;
private BuildAnalysisImporterType[] m_CachedImporterTypes = Array.Empty();
+ private BuildAnalysisAsset[] m_CachedAssets = Array.Empty();
public VisualElement Root => m_Root;
@@ -54,6 +59,9 @@ public void Initialize()
m_AssetsValue = m_Root.Q("stat-card-assets").Q("value");
m_RootAssetsCard = m_Root.Q("stat-card-root-assets");
m_RootAssetsValue = m_RootAssetsCard.Q("value");
+ m_ContentMain = m_Root.Q("assets-content-main");
+ m_AssetSourceBanner = m_Root.Q("asset-source-banner");
+ m_AssetsEmptyState = m_Root.Q("assets-empty-state");
var sections = m_Root.Q("assets-sections");
m_RootAssetTable = new RootAssetTable();
@@ -65,6 +73,7 @@ public void Initialize()
m_InspectorHost.Add(m_AssetInspector);
m_AssetTable.SelectionChanged += OnAssetSelectionChanged;
+ m_RootAssetTable.SelectionChanged += OnRootAssetSelectionChanged;
// Defer first CollapseChild call until after first layout — TwoPaneSplitView
// throws if collapsed before its initial geometry is computed.
@@ -75,13 +84,45 @@ public void Initialize()
private void OnAssetSelectionChanged(BuildAnalysisAsset? asset)
{
- var importerType = asset.HasValue ? ResolveImporterType(asset.Value.ImporterTypeId) : null;
- m_AssetInspector.SetAsset(asset, importerType);
-
if (asset.HasValue)
+ {
+ m_SuppressSelectionClear = true;
+ m_RootAssetTable.ClearSelection();
+ m_SuppressSelectionClear = false;
+
+ m_AssetInspector.ShowAsset(asset.Value, ResolveImporterType(asset.Value.ImporterTypeId));
InspectorOpenRequested?.Invoke();
+ return;
+ }
+
+ if (m_SuppressSelectionClear || m_AssetInspector.CurrentMode != AssetInspector.Mode.Asset)
+ return;
+ m_AssetInspector.ShowEmpty();
}
+ private void OnRootAssetSelectionChanged(BuildAnalysisRootAsset? root)
+ {
+ if (root.HasValue)
+ {
+ m_SuppressSelectionClear = true;
+ m_AssetTable.ClearSelection();
+ m_SuppressSelectionClear = false;
+
+ var r = root.Value;
+ var rootAsset = ResolveAsset(r.AssetId);
+
+ m_AssetInspector.ShowRootAsset(r, rootAsset ?? default, m_CachedAssets);
+ InspectorOpenRequested?.Invoke();
+ return;
+ }
+
+ if (m_SuppressSelectionClear || m_AssetInspector.CurrentMode != AssetInspector.Mode.Root)
+ return;
+ m_AssetInspector.ShowEmpty();
+ }
+
+ private void ResetInspector() => m_AssetInspector.ShowEmpty();
+
private BuildAnalysisImporterType? ResolveImporterType(int id)
{
if (id < 0 || id >= m_CachedImporterTypes.Length)
@@ -89,22 +130,36 @@ private void OnAssetSelectionChanged(BuildAnalysisAsset? asset)
return m_CachedImporterTypes[id];
}
+ private BuildAnalysisAsset? ResolveAsset(int assetId)
+ {
+ if (assetId < 0 || assetId >= m_CachedAssets.Length)
+ return null;
+ return m_CachedAssets[assetId];
+ }
+
public void SetSelection(BuildEntry selection, BuildAnalysis analysis)
{
var hasSelection = selection != null && analysis != null;
m_NoSelection.style.display = hasSelection ? DisplayStyle.None : DisplayStyle.Flex;
m_Body.style.display = hasSelection ? DisplayStyle.Flex : DisplayStyle.None;
+ ResetInspector();
+
if (!hasSelection)
{
m_CachedImporterTypes = Array.Empty();
+ m_CachedAssets = Array.Empty();
m_RootAssetsCard.style.display = DisplayStyle.None;
m_RootAssetTable.style.display = DisplayStyle.None;
+ m_AssetSourceBanner.style.display = DisplayStyle.None;
+ m_AssetsEmptyState.style.display = DisplayStyle.None;
return;
}
- m_CachedImporterTypes = analysis.Tables.ImporterTypes ?? Array.Empty();
- m_Header.Bind(selection, analysis);
+ m_CachedImporterTypes = analysis.Tables.ImporterTypes;
+ m_CachedAssets = analysis.Tables.Assets;
+
+ m_Header.Bind(selection);
var counts = analysis.Computed.Counts;
m_ScenesValue.text = counts.SceneCount.ToString();
m_AssetsValue.text = counts.AssetCount.ToString();
@@ -119,6 +174,30 @@ public void SetSelection(BuildEntry selection, BuildAnalysis analysis)
}
m_AssetTable.Bind(analysis);
+
+ // Assets-less builds (scripts-only / incremental-clean) borrow the table from an earlier build.
+ // A build whose recorded content source couldn't be resolved hides the table and shows the empty state instead.
+ // Reflect that state instead of a bare empty grid.
+ var unavailable = analysis.AssetSource.SourceUnavailable;
+ m_ContentMain.style.display = unavailable ? DisplayStyle.None : DisplayStyle.Flex;
+
+ m_AssetsEmptyState.style.display = unavailable ? DisplayStyle.Flex : DisplayStyle.None;
+ if (unavailable)
+ m_AssetsEmptyState.text = "No asset data was found for this build. " +
+ "Scripts-only and incremental builds show assets from an earlier complete build, but none was found. Run a complete build to record asset data.";
+
+ var borrowed = analysis.AssetSource.IsBorrowed;
+ m_AssetSourceBanner.style.display = borrowed ? DisplayStyle.Flex : DisplayStyle.None;
+ if (borrowed)
+ m_AssetSourceBanner.text = BuildBorrowedBannerText(analysis.AssetSource);
+ }
+
+ private static string BuildBorrowedBannerText(BuildAnalysisAssetSource source)
+ {
+ var date = FormatUtility.TryParseBuildTimestamp(source.BuildStartedAtUtc, out var parsed)
+ ? $" ({FormatUtility.FormatBuildDate(parsed.ToLocalTime().DateTime)})"
+ : string.Empty;
+ return $"The asset data shown is from an earlier complete build{date}. This build did not record any of its own.";
}
public void OnTabVisibilityChanged(bool isVisible)
@@ -129,6 +208,7 @@ public void OnTabVisibilityChanged(bool isVisible)
{
m_AssetTable.ClearSelection();
m_RootAssetTable.ClearSelection();
+ ResetInspector();
}
}
diff --git a/Modules/BuildAnalysis/UI/Tabs/Assets/RootAssetTable.cs b/Modules/BuildAnalysis/UI/Tabs/Assets/RootAssetTable.cs
index 0041995d7c..4f0696488d 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Assets/RootAssetTable.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Assets/RootAssetTable.cs
@@ -15,6 +15,8 @@ internal class RootAssetTable : VisualElement
{
private const string k_UxmlPath = "BuildAnalysis/UXML/RootAssetTable.uxml";
+ public event Action SelectionChanged;
+
private readonly ToolbarSearchField m_SearchField;
private readonly MultiColumnListView m_ListView;
private readonly Label m_FooterCountLabel;
@@ -62,6 +64,12 @@ public RootAssetTable()
m_ListView.columns["column-total-assets"].makeCell = MakeNumericCell;
m_ListView.columns["column-total-assets"].bindCell = BindTotalCountCell;
+ // Column header tooltips
+ m_ListView.Q("column-direct-size").tooltip = "Build output size for this root and what loads immediately with it. Excludes on-demand loadable content.";
+ m_ListView.Q("column-direct-assets").tooltip = "Count of source assets that load immediately with this root. Excludes on-demand loadables.";
+ m_ListView.Q("column-total-size").tooltip = "Build output size for everything reachable from this root, including on-demand loadable content.";
+ m_ListView.Q("column-total-assets").tooltip = "Count of source assets reachable from this root, including on-demand loadables.";
+
m_SearchField.RegisterValueChangedCallback(evt =>
{
var newText = evt.newValue ?? string.Empty;
@@ -73,6 +81,7 @@ public RootAssetTable()
ApplySort();
m_ListView.RefreshItems();
};
+ m_ListView.selectionChanged += OnListSelectionChanged;
}
public void ClearSelection()
@@ -80,10 +89,27 @@ public void ClearSelection()
m_ListView.selectedIndex = -1;
}
+ private void OnListSelectionChanged(IEnumerable items)
+ {
+ BuildAnalysisRootAsset? root = null;
+ if (items != null)
+ {
+ foreach (var item in items)
+ {
+ if (item is int rootIdx and >= 0 && rootIdx < m_RootAssets.Length)
+ {
+ root = m_RootAssets[rootIdx];
+ break;
+ }
+ }
+ }
+ SelectionChanged?.Invoke(root);
+ }
+
public void Bind(BuildAnalysis analysis)
{
- m_RootAssets = analysis.Tables.RootAssets ?? Array.Empty();
- m_Assets = analysis.Tables.Assets ?? Array.Empty();
+ m_RootAssets = analysis.Tables.RootAssets;
+ m_Assets = analysis.Tables.Assets;
var n = m_RootAssets.Length;
EnsureCapacity(ref m_Names, n);
diff --git a/Modules/BuildAnalysis/UI/Tabs/Overview/BuildStepsElement.cs b/Modules/BuildAnalysis/UI/Tabs/Overview/BuildStepsElement.cs
index 8bab348aff..4f4276dbb9 100644
--- a/Modules/BuildAnalysis/UI/Tabs/Overview/BuildStepsElement.cs
+++ b/Modules/BuildAnalysis/UI/Tabs/Overview/BuildStepsElement.cs
@@ -178,11 +178,11 @@ private void BindBadgeCell(VisualElement element, int index)
var errorBadge = element.Q(className: "build-step-badge--error");
warnBadge.style.display = data.WarningCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
- warnBadge.Q(className: "build-step-badge-count").text = FormatUtility.FormatCount(data.WarningCount);
+ warnBadge.Q(className: "build-step-badge-count").text = FormatUtility.FormatCappedCount(data.WarningCount);
warnBadge.tooltip = FormatBadgeTooltip(data.WarningCount, "Warning");
errorBadge.style.display = data.ErrorCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
- errorBadge.Q(className: "build-step-badge-count").text = FormatUtility.FormatCount(data.ErrorCount);
+ errorBadge.Q