first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:36:52 +07:00
commit 6f2eafa33c
14093 changed files with 1253472 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.AI.Navigation.Editor.Tests")]

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b436bc9c47bb45bd98478a478a2608f8
timeCreated: 1707900617

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d25d346918c1247368b4cb66a787b59e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,2 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.AI.Navigation.Updater")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a86108f91b07e4fe3b5cd8fedc9f1797
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System;
namespace Unity.AI.Navigation.Editor.Converter
{
/// <summary>
/// A structure holding the information for each Item that needs to be Converted.
/// Name = The Name of the asset that is being converted.
/// Info = Information that can be used to store some data. This will also be shown in the UI.
/// WarningMessage = If there are some issues with the converter that we already know about.
/// Example: If we know it is a custom shader, we can not convert it so we add the information here.
/// AdditionalData = Additional data that can be used to store some data. This will not be shown in the UI.
/// </summary>
[Serializable]
internal struct ConverterItemDescriptor
{
/// <summary> Name of the asset being converted. This will be shown in the UI. </summary>
public string name;
/// <summary> Information that can be used to store some data. This will also be shown in the UI. </summary>
public string info;
/// <summary> If there are some issues with the converter that we already know about during init phase. This will be added as a tooltip on the warning icon. </summary>
public string warningMessage;
/// <summary> Additional data that can be used to store data related to the item to be converted.</summary>
public string additionalData;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3f72db493d3cd4e03ae748a6cd15534d
timeCreated: 1618229698

View File

@@ -0,0 +1,16 @@
namespace Unity.AI.Navigation.Editor.Converter
{
/// <summary>
/// A structure holding the information for each Item that needs to be Converted.
/// Descriptor = The ConverterItemDescriptor this item contain.
/// Index = The index for this item in the list of converter items.
/// </summary>
internal struct ConverterItemInfo
{
/// <summary> The ConverterItemDescriptor this item contain. </summary>
public ConverterItemDescriptor descriptor { get; internal set; }
/// <summary> The index for this item in the list of converter items. </summary>
public int index { get; internal set; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fac6f414adf2e4556865a180b1529cbb
timeCreated: 1618230361

View File

@@ -0,0 +1,23 @@
using UnityEditor;
using UnityEngine;
namespace Unity.AI.Navigation.Editor.Converter
{
internal static class EditorStyles
{
public static Texture iconHelp;
public static Texture2D iconPending;
public static Texture2D iconWarn;
public static Texture2D iconFail;
public static Texture2D iconSuccess;
static EditorStyles()
{
iconFail = EditorGUIUtility.Load("icons/console.erroricon.png") as Texture2D;
iconWarn = EditorGUIUtility.Load("icons/console.warnicon.png") as Texture2D;
iconHelp = EditorGUIUtility.Load("icons/console.infoicon.png") as Texture2D;
iconSuccess = EditorGUIUtility.FindTexture("TestPassed");
iconPending = EditorGUIUtility.FindTexture("Toolbar Minus");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c0c4a4a1781441ff8c5f9585d5a3462
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Unity.AI.Navigation.Editor.Converter
{
/// <summary>
/// A structure needed for the initialization step of the converter.
/// Stores data to be visible in the UI.
/// </summary>
internal struct InitializeConverterContext
{
/// <summary>
/// Stores the list of ConverterItemDescriptor that will be filled in during the initialization step.
/// </summary>
internal List<ConverterItemDescriptor> items;
/// <summary>
/// Add to the list of assets to be converted.
/// This will be used to display information to the user in the UI.
/// </summary>
/// <param name="item">The item to add to the list items to convert</param>
internal void AddAssetToConvert(ConverterItemDescriptor item)
{
items.Add(item);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a5cf3eb6bee954ca5953487784e68715
timeCreated: 1618230672

View File

@@ -0,0 +1,27 @@
namespace Unity.AI.Navigation.Editor.Converter
{
/// <summary>
/// A structure needed for the conversion part of the converter. <br />
/// This holds the items that are being converted. <br />
/// All arrays are the same length and the index of each array corresponds to the same item.
/// </summary>
internal struct RunItemContext
{
/// <summary> The items that will go through the conversion code. </summary>
public ConverterItemInfo[] items { get; }
/// <summary> A bool to set if these items failed to convert. </summary>
public bool[] didFail { get; }
/// <summary> Info to store data to be shown in the UI. </summary>
public string[] info { get; }
/// <summary> Constructor for the RunItemContext. </summary>
public RunItemContext(ConverterItemInfo[] items)
{
this.items = items;
didFail = new bool[this.items.Length];
info = new string[this.items.Length];
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a841ce8d078944bb8dc02dc43952c91
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
using System;
namespace Unity.AI.Navigation.Editor.Converter
{
internal abstract class SystemConverter
{
/// <summary>
/// Name of the converter.
/// </summary>
public abstract string name { get; }
/// <summary>
/// The information when hovering over the converter.
/// </summary>
public abstract string info { get; }
/// <summary>
/// A check if the converter is enabled or not. Can be used to do a check if prerequisites are met to have it enabled or disabled.
/// </summary>
public virtual bool isEnabled => true;
/// <summary>
/// A priority of the converter. The lower the number (can be negative), the earlier it will be executed. Can be used to make sure that a converter runs before another converter.
/// </summary>
public virtual int priority => 0;
/// <summary>
/// A check to see if the converter needs to create the index.
/// This will only need to be set to true if the converter is using search api, and search queries.
/// If set to true the converter framework will create the indexer and remove it after all search queries are done.
/// </summary>
public virtual bool needsIndexing => false;
/// <summary>
/// This method getting triggered when clicking the listview item in the UI.
/// </summary>
public virtual void OnClicked(int index)
{
}
// This is so that we can have different segment in our UI, example Unity converters, your custom converters etc..
// This is not implemented yet
public virtual string category { get; }
// This is in which drop down item the converter belongs to.
// Not properly implemented yet
public abstract Type container { get; }
/// <summary>
/// This runs when initializing the converter. To gather data for the UI and also for the converter if needed.
/// </summary>
/// <param name="context">The context that will be used to initialize data for the converter.</param>
public abstract void OnInitialize(InitializeConverterContext context, Action callback);
/// <summary>
/// The method that will be run before Run method if needed.
/// </summary>
public virtual void OnPreRun()
{
}
/// <summary>
/// The method that will be run when converting the assets.
/// </summary>
/// <param name="context">The context that will be used when executing converter.</param>
public abstract void OnRun(ref RunItemContext context);
/// <summary>
/// The method that will be run after the converters are done if needed.
/// </summary>
public virtual void OnPostRun()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c9fba05c2cea421a8771ca17afce238
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
namespace Unity.AI.Navigation.Editor.Converter
{
/// <summary>
/// A class to contain converters. This is for a common set of converters.
/// For example: Converters that is for Built-in to URP would have it's own container.
/// </summary>
public abstract class SystemConverterContainer
{
/// <summary>
/// The name of the Container. This will show up int the UI.
/// </summary>
public abstract string name { get; }
/// <summary>
/// The information for this container.
/// This will be shown in the UI to tell the user some information about the converters that this container are targeting.
/// </summary>
public abstract string info { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c78b1d765d234b9c9e3a1d2b0667c47
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,685 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEditor.Search;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using UnityEngine.Assertions;
namespace Unity.AI.Navigation.Editor.Converter
{
// Status for each row item to say in which state they are in.
// This will make sure they are showing the correct icon
[Serializable]
enum Status
{
Pending,
Warning,
Error,
Success
}
// This is the serialized class that stores the state of each item in the list of items to convert
[Serializable]
class ConverterItemState
{
public bool isActive;
// Message that will be displayed on the icon if warning or failed.
public string message;
// Status of the converted item, Pending, Warning, Error or Success
public Status status;
internal bool hasConverted = false;
}
// Each converter uses the active bool
// Each converter has a list of active items/assets
// We do this so that we can use the binding system of the UI Elements
[Serializable]
class ConverterState
{
// This is the enabled state of the whole converter
public bool isEnabled;
public bool isActive;
public bool isLoading; // to name
public bool isInitialized;
public List<ConverterItemState> items = new List<ConverterItemState>();
public int pending;
public int warnings;
public int errors;
public int success;
internal int index;
public bool isActiveAndEnabled => isEnabled && isActive;
public bool requiresInitialization => !isInitialized && isActiveAndEnabled;
}
[Serializable]
internal struct ConverterItems
{
public List<ConverterItemDescriptor> itemDescriptors;
}
[Serializable]
internal class SystemConvertersEditor : EditorWindow
{
public VisualTreeAsset converterEditorAsset;
public VisualTreeAsset converterListAsset;
public VisualTreeAsset converterItem;
ScrollView m_ScrollView;
List<SystemConverter> m_CoreConvertersList = new List<SystemConverter>();
private bool convertButtonActive = false;
// This list needs to be as long as the amount of converters
List<ConverterItems> m_ItemsToConvert = new List<ConverterItems>();
SerializedObject m_SerializedObject;
List<string> m_ContainerChoices = new List<string>();
List<SystemConverterContainer> m_Containers = new List<SystemConverterContainer>();
int m_ContainerChoiceIndex = 0;
// This is a list of Converter States which holds a list of which converter items/assets are active
// There is one for each Converter.
[SerializeField] List<ConverterState> m_ConverterStates = new List<ConverterState>();
TypeCache.TypeCollection m_ConverterContainers;
// Name of the index file
string m_ConverterIndex = "SystemConverterIndex";
public void DontSaveToLayout(EditorWindow wnd)
{
// Making sure that the window is not saved in layouts.
Assembly assembly = typeof(EditorWindow).Assembly;
var editorWindowType = typeof(EditorWindow);
var hostViewType = assembly.GetType("UnityEditor.HostView");
var containerWindowType = assembly.GetType("UnityEditor.ContainerWindow");
var parentViewField = editorWindowType.GetField("m_Parent", BindingFlags.Instance | BindingFlags.NonPublic);
var parentViewValue = parentViewField.GetValue(wnd);
// window should not be saved to layout
var containerWindowProperty =
hostViewType.GetProperty("window", BindingFlags.Instance | BindingFlags.Public);
var parentContainerWindowValue = containerWindowProperty.GetValue(parentViewValue);
var dontSaveToLayoutField =
containerWindowType.GetField("m_DontSaveToLayout", BindingFlags.Instance | BindingFlags.NonPublic);
dontSaveToLayoutField.SetValue(parentContainerWindowValue, true);
}
void OnEnable()
{
InitIfNeeded();
}
void InitIfNeeded()
{
if (m_CoreConvertersList.Any())
return;
m_CoreConvertersList = new List<SystemConverter>();
// This is the drop down choices.
m_ConverterContainers = TypeCache.GetTypesDerivedFrom<SystemConverterContainer>();
foreach (var containerType in m_ConverterContainers)
{
var container = (SystemConverterContainer)Activator.CreateInstance(containerType);
m_Containers.Add(container);
m_ContainerChoices.Add(container.name);
}
if (m_ConverterContainers.Any())
{
GetConverters();
}
else
{
ClearConverterStates();
}
}
void ClearConverterStates()
{
m_CoreConvertersList.Clear();
m_ConverterStates.Clear();
m_ItemsToConvert.Clear();
}
void GetConverters()
{
ClearConverterStates();
var converterList = TypeCache.GetTypesDerivedFrom<SystemConverter>();
for (var i = 0; i < converterList.Count; ++i)
{
// Iterate over the converters that are used by the current container
var conv = (SystemConverter)Activator.CreateInstance(converterList[i]);
if (conv.container == m_ConverterContainers[m_ContainerChoiceIndex])
{
m_CoreConvertersList.Add(conv);
}
}
// this need to be sorted by Priority property
m_CoreConvertersList = m_CoreConvertersList
.OrderBy(o => o.priority).ToList();
for (var i = 0; i < m_CoreConvertersList.Count; i++)
{
// Create a new ConvertState which holds the active state of the converter
var converterState = new ConverterState
{
isEnabled = m_CoreConvertersList[i].isEnabled,
isActive = true,
isInitialized = false,
items = new List<ConverterItemState>(),
index = i,
};
m_ConverterStates.Add(converterState);
// This just creates empty entries in the m_ItemsToConvert.
// This list need to have the same amount of entries as the converters
var converterItemInfos = new List<ConverterItemDescriptor>();
//m_ItemsToConvert.Add(converterItemInfos);
m_ItemsToConvert.Add(new ConverterItems { itemDescriptors = converterItemInfos });
}
}
public void CreateGUI()
{
InitIfNeeded();
if (m_ConverterContainers.Any())
{
m_SerializedObject = new SerializedObject(this);
converterEditorAsset.CloneTree(rootVisualElement);
RecreateUI();
var button = rootVisualElement.Q<Button>("convertButton");
button.RegisterCallback<ClickEvent>(Convert);
button.SetEnabled(false);
var initButton = rootVisualElement.Q<Button>("initializeButton");
initButton.RegisterCallback<ClickEvent>(InitializeAllActiveConverters);
}
}
void RecreateUI()
{
m_SerializedObject.Update();
var currentContainer = m_Containers[m_ContainerChoiceIndex];
rootVisualElement.Q<Label>("conversionName").text = currentContainer.name;
rootVisualElement.Q<TextElement>("conversionInfo").text = currentContainer.info;
rootVisualElement.Q<Image>("converterContainerHelpIcon").image = EditorStyles.iconHelp;
// Getting the scrollview where the converters should be added
m_ScrollView = rootVisualElement.Q<ScrollView>("convertersScrollView");
m_ScrollView.Clear();
for (int i = 0; i < m_CoreConvertersList.Count; ++i)
{
// Making an item using the converterListAsset as a template.
// Then adding the information needed for each converter
VisualElement item = new VisualElement();
converterListAsset.CloneTree(item);
var conv = m_CoreConvertersList[i];
item.SetEnabled(conv.isEnabled);
item.Q<Label>("converterName").text = conv.name;
item.Q<Label>("converterInfo").text = conv.info;
// setup the images
item.Q<Image>("pendingImage").image = EditorStyles.iconPending;
item.Q<Image>("pendingImage").tooltip = "Pending";
var pendingLabel = item.Q<Label>("pendingLabel");
item.Q<Image>("warningImage").image = EditorStyles.iconWarn;
item.Q<Image>("warningImage").tooltip = "Warnings";
var warningLabel = item.Q<Label>("warningLabel");
item.Q<Image>("errorImage").image = EditorStyles.iconFail;
item.Q<Image>("errorImage").tooltip = "Failed";
var errorLabel = item.Q<Label>("errorLabel");
item.Q<Image>("successImage").image = EditorStyles.iconSuccess;
item.Q<Image>("successImage").tooltip = "Success";
var successLabel = item.Q<Label>("successLabel");
var converterEnabledToggle = item.Q<Toggle>("converterEnabled");
converterEnabledToggle.bindingPath =
$"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.isActive)}";
pendingLabel.bindingPath =
$"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.pending)}";
warningLabel.bindingPath =
$"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.warnings)}";
errorLabel.bindingPath =
$"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.errors)}";
successLabel.bindingPath =
$"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.success)}";
VisualElement child = item;
ListView listView = child.Q<ListView>("converterItems");
listView.showBoundCollectionSize = false;
listView.bindingPath = $"{nameof(m_ConverterStates)}.Array.data[{i}].{nameof(ConverterState.items)}";
int id = i;
listView.makeItem = () =>
{
var convertItem = converterItem.CloneTree();
// Adding the contextual menu for each item
convertItem.AddManipulator(new ContextualMenuManipulator(evt => AddToContextMenu(evt, id)));
return convertItem;
};
listView.bindItem = (element, index) =>
{
m_SerializedObject.Update();
var property = m_SerializedObject.FindProperty($"{listView.bindingPath}.Array.data[{index}]");
// ListView doesn't bind the child elements for us properly, so we do that for it
// In the UXML our root is a BindableElement, as we can't bind otherwise.
var bindable = (BindableElement)element;
bindable.BindProperty(property);
// Adding index here to userData so it can be retrieved later
element.userData = index;
var status = (Status)property.FindPropertyRelative("status").enumValueIndex;
var info = property.FindPropertyRelative("message").stringValue;
// Update the amount of things to convert
child.Q<Label>("converterStats").text = $"{m_ItemsToConvert[id].itemDescriptors.Count} items";
var convItemDesc = m_ItemsToConvert[id].itemDescriptors[index];
element.Q<Label>("converterItemName").text = convItemDesc.name;
element.Q<Label>("converterItemPath").text = convItemDesc.info;
// Changing the icon here depending on the status.
Texture2D icon = null;
switch (status)
{
case Status.Pending:
icon = EditorStyles.iconPending;
break;
case Status.Error:
icon = EditorStyles.iconFail;
break;
case Status.Warning:
icon = EditorStyles.iconWarn;
break;
case Status.Success:
icon = EditorStyles.iconSuccess;
break;
}
element.Q<Image>("converterItemStatusIcon").image = icon;
element.Q<Image>("converterItemStatusIcon").tooltip = info;
};
listView.selectionChanged += obj => { m_CoreConvertersList[id].OnClicked(listView.selectedIndex); };
listView.unbindItem = (element, index) =>
{
var bindable = (BindableElement)element;
bindable.Unbind();
};
m_ScrollView.Add(item);
}
rootVisualElement.Bind(m_SerializedObject);
var button = rootVisualElement.Q<Button>("convertButton");
button.RegisterCallback<ClickEvent>(Convert);
button.SetEnabled(convertButtonActive);
var initButton = rootVisualElement.Q<Button>("initializeButton");
initButton.RegisterCallback<ClickEvent>(InitializeAllActiveConverters);
}
void GetAndSetData(int i, Action onAllConvertersCompleted = null)
{
// This need to be in Init method
// Need to get the assets that this converter is converting.
// Need to return Name, Path, Initial info, Help link.
// New empty list of ConverterItemInfos
List<ConverterItemDescriptor> converterItemInfos = new List<ConverterItemDescriptor>();
var initCtx = new InitializeConverterContext { items = converterItemInfos };
var conv = m_CoreConvertersList[i];
m_ConverterStates[i].isLoading = true;
// This should also go to the init method
// This will fill out the converter item infos list
int id = i;
conv.OnInitialize(initCtx, OnConverterCompleteDataCollection);
void OnConverterCompleteDataCollection()
{
// Set the item infos list to to the right index
m_ItemsToConvert[id] = new ConverterItems { itemDescriptors = converterItemInfos };
m_ConverterStates[id].items = new List<ConverterItemState>(converterItemInfos.Count);
// Default all the entries to true
for (var j = 0; j < converterItemInfos.Count; j++)
{
string message = string.Empty;
Status status;
bool active = true;
// If this data hasn't been filled in from the init phase then we can assume that there are no issues / warnings
if (string.IsNullOrEmpty(converterItemInfos[j].warningMessage))
{
status = Status.Pending;
}
else
{
status = Status.Warning;
message = converterItemInfos[j].warningMessage;
active = false;
m_ConverterStates[id].warnings++;
}
m_ConverterStates[id].items.Add(new ConverterItemState
{
isActive = active,
message = message,
status = status,
hasConverted = false,
});
}
m_ConverterStates[id].isLoading = false;
m_ConverterStates[id].isInitialized = true;
// Making sure that the pending amount is set to the amount of items needs converting
m_ConverterStates[id].pending = m_ConverterStates[id].items.Count;
EditorUtility.SetDirty(this);
m_SerializedObject.ApplyModifiedProperties();
CheckAllConvertersCompleted();
convertButtonActive = true;
// Make sure that the Convert Button is turned back on
var button = rootVisualElement.Q<Button>("convertButton");
button.SetEnabled(convertButtonActive);
}
void CheckAllConvertersCompleted()
{
int convertersToInitialize = 0;
int convertersInitialized = 0;
for (var j = 0; j < m_ConverterStates.Count; j++)
{
var converter = m_ConverterStates[j];
// Skip inactive converters
if (!converter.isActiveAndEnabled)
continue;
if (converter.isInitialized)
convertersInitialized++;
else
convertersToInitialize++;
}
var sum = convertersToInitialize + convertersInitialized;
Assert.IsFalse(sum == 0);
// Show our progress so far
EditorUtility.ClearProgressBar();
EditorUtility.DisplayProgressBar($"Initializing converters", $"Initializing converters ({convertersInitialized}/{sum})...", (float)convertersInitialized / sum);
// If all converters are initialized call the complete callback
if (convertersToInitialize == 0)
{
onAllConvertersCompleted?.Invoke();
}
}
}
void InitializeAllActiveConverters(ClickEvent evt)
{
// If we use search index, go async
if (ShouldCreateSearchIndex())
{
CreateSearchIndex(m_ConverterIndex);
}
// Otherwise do everything directly
else
{
ConverterCollectData(() => { EditorUtility.ClearProgressBar(); });
}
void CreateSearchIndex(string name)
{
// Create <guid>.index in the project
var title = $"Building {name} search index";
EditorUtility.DisplayProgressBar(title, "Creating search index...", -1f);
// Private implementation of a file naming function which puts the file at the selected path.
Type assetdatabase = typeof(AssetDatabase);
var indexPath = (string)assetdatabase.GetMethod("GetUniquePathNameAtSelectedPath", BindingFlags.NonPublic | BindingFlags.Static).Invoke(assetdatabase, new object[] { $"Assets/{name}.index" });
// Write search index manifest
System.IO.File.WriteAllText(indexPath,
@"{
""roots"": [""Assets""],
""includes"": [],
""excludes"": [],
""options"": {
""types"": true,
""properties"": true,
""extended"": true,
""dependencies"": true
},
""baseScore"": 9999
}");
// Import the search index
AssetDatabase.ImportAsset(indexPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.DontDownloadFromCacheServer);
EditorApplication.delayCall += () =>
{
// Create dummy request to ensure indexing has finished
var context = SearchService.CreateContext("asset", $"p: a=\"{name}\"");
SearchService.Request(context, (_, items) =>
{
OnSearchIndexCreated(name, indexPath, () =>
{
DeleteSearchIndex(context, indexPath);
});
});
};
}
void OnSearchIndexCreated(string name, string path, Action onComplete)
{
EditorUtility.ClearProgressBar();
ConverterCollectData(onComplete);
}
void ConverterCollectData(Action onConverterDataCollectionComplete)
{
EditorUtility.DisplayProgressBar($"Initializing converters", $"Initializing converters...", -1f);
var convertersToInitialize = 0;
for (var i = 0; i < m_ConverterStates.Count; ++i)
{
if (m_ConverterStates[i].isEnabled)
{
GetAndSetData(i, onConverterDataCollectionComplete);
convertersToInitialize++;
}
}
// If we don't have any converters to initialize, call the complete callback directly.
if (convertersToInitialize == 0)
onConverterDataCollectionComplete?.Invoke();
}
void DeleteSearchIndex(SearchContext context, string indexPath)
{
context?.Dispose();
// Client code has finished with the created index. We can delete it.
AssetDatabase.DeleteAsset(indexPath);
EditorUtility.ClearProgressBar();
}
}
bool ShouldCreateSearchIndex()
{
for (int i = 0; i < m_ConverterStates.Count; ++i)
{
if (m_ConverterStates[i].requiresInitialization)
{
var converter = m_CoreConvertersList[i];
if (converter.needsIndexing)
{
return true;
}
}
}
return false;
}
void AddToContextMenu(ContextualMenuPopulateEvent evt, int coreConverterIndex)
{
var ve = (VisualElement)evt.target;
// Checking if this context menu should be enabled or not
var isActive = m_ConverterStates[coreConverterIndex].items[(int)ve.userData].isActive &&
!m_ConverterStates[coreConverterIndex].items[(int)ve.userData].hasConverted;
evt.menu.AppendAction("Run converter for this asset",
e => { ConvertIndex(coreConverterIndex, (int)ve.userData); },
isActive ? DropdownMenuAction.AlwaysEnabled : DropdownMenuAction.AlwaysDisabled);
}
void Convert(ClickEvent evt)
{
var activeConverterStates = new List<ConverterState>();
// Get the names of the converters
// Get the amount of them
// Make the string "name x/y"
// Getting all the active converters to use in the cancelable progressbar
foreach (var state in m_ConverterStates)
{
if (state.isActive && state.isInitialized)
activeConverterStates.Add(state);
}
var converterCount = 0;
var activeConvertersCount = activeConverterStates.Count;
foreach (var activeConverterState in activeConverterStates)
{
if (activeConverterState.items.Count == 0)
continue;
var hasItemsToConvert = false;
foreach (var item in activeConverterState.items)
{
if (item.isActive && !item.hasConverted)
{
hasItemsToConvert = true;
break;
}
}
if (!hasItemsToConvert)
continue;
var converterIndex = activeConverterState.index;
m_CoreConvertersList[converterIndex].OnPreRun();
var converterName = m_CoreConvertersList[converterIndex].name;
var progressTitle = $"{converterName} Converter : {converterCount++}/{activeConvertersCount}";
BatchConvert(activeConverterState, progressTitle);
m_CoreConvertersList[converterIndex].OnPostRun();
AssetDatabase.SaveAssets();
EditorUtility.ClearProgressBar();
}
}
void BatchConvert(ConverterState converterState, string progressBarTile)
{
var converterIndex = converterState.index;
var itemsFound = converterState.items;
var itemsToConvert = new List<ConverterItemInfo>(itemsFound.Count);
for (var i = 0; i < itemsFound.Count; ++i)
{
if (itemsFound[i].isActive && !itemsFound[i].hasConverted)
itemsToConvert.Add(new ConverterItemInfo()
{
index = i,
descriptor = m_ItemsToConvert[converterIndex].itemDescriptors[i]
});
}
// Since this is a batched process, we don't have progress to show, so we stick it to 50%.
EditorUtility.DisplayProgressBar(progressBarTile, $"Processing {itemsToConvert.Count} items.", 0.5f);
var ctx = new RunItemContext(itemsToConvert.ToArray());
m_CoreConvertersList[converterIndex].OnRun(ref ctx);
UpdateInfo(converterIndex, ctx);
}
void ConvertIndex(int converterIndex, int index)
{
if (!m_ConverterStates[converterIndex].items[index].hasConverted)
{
m_ConverterStates[converterIndex].items[index].hasConverted = true;
var item = new ConverterItemInfo()
{
index = index,
descriptor = m_ItemsToConvert[converterIndex].itemDescriptors[index],
};
var ctx = new RunItemContext(new[] { item });
m_CoreConvertersList[converterIndex].OnRun(ref ctx);
UpdateInfo(converterIndex, ctx);
}
}
void UpdateInfo(int converterIndex, RunItemContext ctx)
{
// Reset converter stats, so that they don't contain old data from previous runs.
m_ConverterStates[converterIndex].warnings = 0;
m_ConverterStates[converterIndex].errors = 0;
m_ConverterStates[converterIndex].success = 0;
var items = ctx.items;
for (var i = 0; i < items.Length; ++i)
{
var itemIndex = items[i].index;
if (ctx.didFail[i])
{
m_ConverterStates[converterIndex].items[itemIndex].message = ctx.info[i];
m_ConverterStates[converterIndex].items[itemIndex].status = Status.Error;
m_ConverterStates[converterIndex].errors++;
}
else
{
m_ConverterStates[converterIndex].items[itemIndex].status = Status.Success;
m_ConverterStates[converterIndex].success++;
m_ConverterStates[converterIndex].items[itemIndex].hasConverted = true;
}
// If the item was converted, we deselect it in the conversion list.
m_ConverterStates[converterIndex].items[itemIndex].isActive = false;
}
if (m_ConverterStates[converterIndex].pending > 0)
m_ConverterStates[converterIndex].pending--;
var child = m_ScrollView[converterIndex];
child.Q<ListView>("converterItems").Rebuild();
}
}
}

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 030df50ef7a3d40deaa58109d30100c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- converterEditorAsset: {fileID: 9197481963319205126, guid: cd1eb3c3c695c494d855ea678fe7395b,
type: 3}
- converterListAsset: {fileID: 9197481963319205126, guid: aaa3e510761864dac9b71f85526490d6,
type: 3}
- converterItem: {fileID: 9197481963319205126, guid: d6de7697d63d64fabbfb31425d93541e,
type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
{
"name": "Unity.AI.Navigation.Editor.ConversionSystem",
"rootNamespace": "",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c57630adab7a340ec94f10e4fcac12f1
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
.convertersListView {
--unity-item-height: 16;
height: 120px;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3eb99c80b411349188e1e5167887ac6c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,19 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<ui:VisualElement style="max-width: 620px; min-width: 620px; flex-grow: 1; padding-left: 19px; padding-right: 1px; padding-top: 15px;">
<ui:VisualElement style="height: 25px; width: 604px; flex-shrink: 0; flex-direction: row;">
<ui:Label name="conversionName" style="-unity-font-style: bold; font-size: 16px; width: 593px; padding-right: 16px; flex-grow: 1; flex-shrink: 1;" />
<ui:Image name="converterContainerHelpIcon" />
</ui:VisualElement>
<ui:TextElement name="conversionInfo" style="height: 46px; width: 606px; flex-shrink: 0;" />
<ui:HelpBox message-type="Warning" text="This process makes irreversible changes to the project. Back up your project before proceeding." />
<ui:VisualElement style="flex-direction: row;">
<ui:VisualElement style="flex-grow: 1;" />
</ui:VisualElement>
<ui:ScrollView scroll-deceleration-rate="0,135" elasticity="0,1" name="convertersScrollView" style="flex-grow: 1; flex-shrink: 1; max-width: none; width: 632px; padding-right: 0; padding-left: 4px;" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row-reverse; flex-shrink: 0; padding-left: 15px; padding-right: 14px;">
<ui:Button text=" Convert Assets" name="convertButton" style="flex-direction: column; margin-bottom: 15px;" />
<ui:Label style="flex-grow: 1;" />
<ui:Button text="Initialize Converters" name="initializeButton" tooltip="This will initialize all the converters that have been toggled on." style="flex-direction: column; margin-bottom: 15px;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: cd1eb3c3c695c494d855ea678fe7395b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63cb84b97408b4d25a6925daccffcd22
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,24 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<ui:VisualElement name="converterTopVisualElement" style="flex-grow: 1; height: 250px; width: 606px; flex-shrink: 0; border-bottom-width: 2px; border-top-width: 2px; border-bottom-color: rgb(0, 0, 0); margin-bottom: 20px; padding-bottom: 0; margin-top: 20px; padding-right: 0; border-left-width: 2px; border-right-width: 2px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0);">
<ui:VisualElement style="width: 613px; flex-direction: row; flex-grow: 0; margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; flex-shrink: 0; padding-bottom: 4px; padding-left: 2px; border-top-width: 0; border-top-color: rgb(0, 0, 0); padding-top: 11px;">
<ui:Toggle name="converterEnabled" value="false" tooltip="Enabling this will start the pre-processing of this converter." style="flex-grow: 0; height: 24px; margin-top: 0; margin-bottom: 0;" />
<ui:Label name="converterName" text="Name Of The Converter" style="width: 143px; -unity-text-align: middle-left; flex-grow: 1; flex-direction: column; max-height: 20%; height: 20px; min-height: 20px; padding-top: 3px; -unity-font-style: bold; padding-left: 4px;" />
<ui:Label name="converterStats" style="flex-grow: 0; -unity-text-align: middle-right; -unity-font-style: bold; padding-right: 20px;" />
</ui:VisualElement>
<ui:VisualElement style="width: 596px; flex-direction: row; flex-grow: 0; flex-shrink: 1; padding-right: 0; padding-left: 2px; padding-bottom: 4px; overflow: hidden;">
<ui:Label text="info" name="converterInfo" style="-unity-text-align: middle-left; flex-grow: 1; flex-wrap: nowrap; overflow: visible; white-space: normal; padding-top: 0;" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row; height: 19px; flex-grow: 0; margin-top: 0; margin-bottom: 0; padding-right: 14px; width: 608px; flex-shrink: 1;">
<ui:Label text="&#10;" style="flex-grow: 1;" />
<ui:Image name="pendingImage" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px;" />
<ui:Label name="pendingLabel" />
<ui:Image name="warningImage" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px;" />
<ui:Label name="warningLabel" />
<ui:Image name="errorImage" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px;" />
<ui:Label name="errorLabel" />
<ui:Image name="successImage" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px;" />
<ui:Label name="successLabel" style="flex-grow: 0; flex-shrink: 0;" />
</ui:VisualElement>
<ui:ListView focusable="true" name="converterItems" show-alternating-row-backgrounds="All" text="Info" style="flex-grow: 1; flex-shrink: 0; height: 100px; width: 602px; padding-bottom: 0; margin-bottom: 0; margin-top: 0;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: aaa3e510761864dac9b71f85526490d6
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,11 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<ui:BindableElement style="flex-grow: 1; flex-direction: row; align-items: center; flex-shrink: 0; padding-right: 20px;">
<ui:Toggle value="true" name="converterItemActive" binding-path="isActive" />
<ui:Label name="converterItemName" text="name" style="flex-grow: 0; width: 200px; overflow: hidden; padding-left: 4px;" />
<ui:Label name="converterItemInfo" style="visibility: hidden;" />
<ui:Label name="converterItemPath" text="path..." style="flex-grow: 0; flex-wrap: nowrap; white-space: nowrap; width: 300px; overflow: hidden; padding-left: 21px;" />
<ui:Label display-tooltip-when-elided="true" style="flex-grow: 1;" />
<ui:Image name="converterItemStatusIcon" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px; justify-content: center; flex-grow: 0; width: 16px; height: 16px;" />
<ui:Image name="converterItemHelpIcon" style="max-width: 16px; max-height: 16px; min-width: 16px; min-height: 16px; justify-content: center; flex-grow: 0;" />
</ui:BindableElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d6de7697d63d64fabbfb31425d93541e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,364 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
namespace Unity.AI.Navigation.Editor
{
/// <summary>
/// Manages assets and baking operation of the NavMesh
/// </summary>
public class NavMeshAssetManager : ScriptableSingleton<NavMeshAssetManager>
{
internal struct AsyncBakeOperation
{
internal NavMeshSurface surface;
internal NavMeshData bakeData;
internal AsyncOperation bakeOperation;
internal int progressReportId;
}
List<AsyncBakeOperation> m_BakeOperations = new List<AsyncBakeOperation>();
internal List<AsyncBakeOperation> GetBakeOperations() { return m_BakeOperations; }
struct SavedPrefabNavMeshData
{
internal NavMeshSurface surface;
internal NavMeshData navMeshData;
}
List<SavedPrefabNavMeshData> m_PrefabNavMeshDataAssets = new List<SavedPrefabNavMeshData>();
static string GetAndEnsureTargetPath(NavMeshSurface surface)
{
// Create directory for the asset if it does not exist yet.
var activeScenePath = surface.gameObject.scene.path;
var targetPath = "Assets";
if (!string.IsNullOrEmpty(activeScenePath))
{
targetPath = Path.Combine(Path.GetDirectoryName(activeScenePath), Path.GetFileNameWithoutExtension(activeScenePath));
}
else if (surface.IsPartOfPrefab())
{
var prefabStage = PrefabStageUtility.GetPrefabStage(surface.gameObject);
var assetPath = prefabStage.assetPath;
if (!string.IsNullOrEmpty(assetPath))
{
var prefabDirectoryName = Path.GetDirectoryName(assetPath);
if (!string.IsNullOrEmpty(prefabDirectoryName))
targetPath = prefabDirectoryName;
}
}
if (!Directory.Exists(targetPath))
Directory.CreateDirectory(targetPath);
return targetPath;
}
static void CreateNavMeshAsset(NavMeshSurface surface)
{
var targetPath = GetAndEnsureTargetPath(surface);
var combinedAssetPath = Path.Combine(targetPath, "NavMesh-" + surface.name + ".asset");
combinedAssetPath = AssetDatabase.GenerateUniqueAssetPath(combinedAssetPath);
AssetDatabase.CreateAsset(surface.navMeshData, combinedAssetPath);
}
NavMeshData GetNavMeshAssetToDelete(NavMeshSurface navSurface)
{
if (PrefabUtility.IsPartOfPrefabInstance(navSurface) && !PrefabUtility.IsPartOfModelPrefab(navSurface))
{
// Don't allow deleting the asset belonging to the prefab parent
var parentSurface = PrefabUtility.GetCorrespondingObjectFromSource(navSurface) as NavMeshSurface;
if (parentSurface && navSurface.navMeshData == parentSurface.navMeshData)
return null;
}
// Do not delete the NavMeshData asset referenced from a prefab until the prefab is saved
if (navSurface.IsPartOfPrefab() && IsCurrentPrefabNavMeshDataStored(navSurface))
return null;
return navSurface.navMeshData;
}
void ClearSurface(NavMeshSurface navSurface)
{
var hasNavMeshData = navSurface.navMeshData != null;
StoreNavMeshDataIfInPrefab(navSurface);
var assetToDelete = GetNavMeshAssetToDelete(navSurface);
navSurface.RemoveData();
if (hasNavMeshData)
{
SetNavMeshData(navSurface, null);
EditorSceneManager.MarkSceneDirty(navSurface.gameObject.scene);
}
if (assetToDelete)
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(assetToDelete));
}
/// <summary>
/// Start baking a list of NavMeshSurface
/// </summary>
/// <param name="surfaces">List of surfaces</param>
public void StartBakingSurfaces(Object[] surfaces)
{
// Remove first to avoid double registration of the callback
EditorApplication.update -= UpdateAsyncBuildOperations;
EditorApplication.update += UpdateAsyncBuildOperations;
foreach (NavMeshSurface surf in surfaces)
{
StoreNavMeshDataIfInPrefab(surf);
var oper = new AsyncBakeOperation();
oper.bakeData = InitializeBakeData(surf);
oper.bakeOperation = surf.UpdateNavMesh(oper.bakeData);
oper.surface = surf;
oper.progressReportId = Progress.Start(L10n.Tr("Baking a NavMesh"), string.Format(L10n.Tr("Surface held by {0} for agent type {1}"), surf.gameObject.name, NavMesh.GetSettingsNameFromID(surf.agentTypeID)));
Progress.RegisterCancelCallback(oper.progressReportId, () =>
{
NavMeshBuilder.Cancel(oper.bakeData);
return true;
});
m_BakeOperations.Add(oper);
}
}
static NavMeshData InitializeBakeData(NavMeshSurface surface)
{
var emptySources = new List<NavMeshBuildSource>();
var emptyBounds = new Bounds();
return UnityEngine.AI.NavMeshBuilder.BuildNavMeshData(surface.GetBuildSettings(), emptySources, emptyBounds
, surface.transform.position, surface.transform.rotation);
}
void UpdateAsyncBuildOperations()
{
foreach (var oper in m_BakeOperations)
{
if (oper.surface == null || oper.bakeOperation == null)
continue;
if (oper.bakeOperation.isDone)
{
var surface = oper.surface;
var delete = GetNavMeshAssetToDelete(surface);
if (delete != null)
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(delete));
surface.RemoveData();
SetNavMeshData(surface, oper.bakeData);
if (surface.isActiveAndEnabled)
surface.AddData();
CreateNavMeshAsset(surface);
if (!EditorApplication.isPlaying || surface.IsPartOfPrefab())
EditorSceneManager.MarkSceneDirty(surface.gameObject.scene);
Progress.Finish(oper.progressReportId);
}
Progress.Report(oper.progressReportId, oper.bakeOperation.progress);
}
m_BakeOperations.RemoveAll(o => o.bakeOperation == null || o.bakeOperation.isDone);
if (m_BakeOperations.Count == 0)
EditorApplication.update -= UpdateAsyncBuildOperations;
}
/// <summary>
/// Checks if an operation of baking is in progress for a specified surface
/// </summary>
/// <param name="surface">A NavMesh surface</param>
/// <returns>True if the specified surface is baking</returns>
public bool IsSurfaceBaking(NavMeshSurface surface)
{
if (surface == null)
return false;
foreach (var oper in m_BakeOperations)
{
if (oper.surface == null || oper.bakeOperation == null)
continue;
if (oper.surface == surface)
return true;
}
return false;
}
/// <summary>
/// Clear NavMesh surfaces
/// </summary>
/// <param name="surfaces">List of surfaces</param>
public void ClearSurfaces(Object[] surfaces)
{
foreach (NavMeshSurface s in surfaces)
ClearSurface(s);
}
static void SetNavMeshData(NavMeshSurface navSurface, NavMeshData navMeshData)
{
var so = new SerializedObject(navSurface);
var navMeshDataProperty = so.FindProperty("m_NavMeshData");
navMeshDataProperty.objectReferenceValue = navMeshData;
so.ApplyModifiedPropertiesWithoutUndo();
}
void StoreNavMeshDataIfInPrefab(NavMeshSurface surfaceToStore)
{
if (!surfaceToStore.IsPartOfPrefab())
return;
// check if data has already been stored for this surface
foreach (var storedAssetInfo in m_PrefabNavMeshDataAssets)
if (storedAssetInfo.surface == surfaceToStore)
return;
if (m_PrefabNavMeshDataAssets.Count == 0)
{
PrefabStage.prefabSaving -= DeleteStoredNavMeshDataAssetsForOwnedSurfaces;
PrefabStage.prefabSaving += DeleteStoredNavMeshDataAssetsForOwnedSurfaces;
PrefabStage.prefabStageClosing -= ForgetUnsavedNavMeshDataChanges;
PrefabStage.prefabStageClosing += ForgetUnsavedNavMeshDataChanges;
}
var isDataOwner = true;
if (PrefabUtility.IsPartOfPrefabInstance(surfaceToStore) && !PrefabUtility.IsPartOfModelPrefab(surfaceToStore))
{
var basePrefabSurface = PrefabUtility.GetCorrespondingObjectFromSource(surfaceToStore) as NavMeshSurface;
isDataOwner = basePrefabSurface == null || surfaceToStore.navMeshData != basePrefabSurface.navMeshData;
}
m_PrefabNavMeshDataAssets.Add(new SavedPrefabNavMeshData { surface = surfaceToStore, navMeshData = isDataOwner ? surfaceToStore.navMeshData : null });
}
bool IsCurrentPrefabNavMeshDataStored(NavMeshSurface surface)
{
if (surface == null)
return false;
foreach (var storedAssetInfo in m_PrefabNavMeshDataAssets)
{
if (storedAssetInfo.surface == surface)
return storedAssetInfo.navMeshData == surface.navMeshData;
}
return false;
}
void DeleteStoredNavMeshDataAssetsForOwnedSurfaces(GameObject gameObjectInPrefab)
{
// Debug.LogFormat("DeleteStoredNavMeshDataAsset() when saving prefab {0}", gameObjectInPrefab.name);
var surfaces = gameObjectInPrefab.GetComponentsInChildren<NavMeshSurface>(true);
foreach (var surface in surfaces)
DeleteStoredPrefabNavMeshDataAsset(surface);
}
void DeleteStoredPrefabNavMeshDataAsset(NavMeshSurface surface)
{
for (var i = m_PrefabNavMeshDataAssets.Count - 1; i >= 0; i--)
{
var storedAssetInfo = m_PrefabNavMeshDataAssets[i];
if (storedAssetInfo.surface == surface)
{
var storedNavMeshData = storedAssetInfo.navMeshData;
if (storedNavMeshData != null && storedNavMeshData != surface.navMeshData)
{
if (!EditorApplication.isPlaying)
{
var assetPath = AssetDatabase.GetAssetPath(storedNavMeshData);
AssetDatabase.DeleteAsset(assetPath);
}
else
{
Debug.LogWarning(
$"The asset of the previous NavMesh data ({storedNavMeshData.name}), owned by " +
$"the prefab that was saved just now ({storedAssetInfo.surface.gameObject.transform.root.gameObject.name}), " +
"cannot be deleted automatically because it might still be used by " +
"NavMeshSurface components in the running scenes. You can safely delete " +
$"the \"{storedNavMeshData.name}.asset\" file after playmode ends.", storedNavMeshData);
}
}
m_PrefabNavMeshDataAssets.RemoveAt(i);
break;
}
}
if (m_PrefabNavMeshDataAssets.Count == 0)
{
PrefabStage.prefabSaving -= DeleteStoredNavMeshDataAssetsForOwnedSurfaces;
PrefabStage.prefabStageClosing -= ForgetUnsavedNavMeshDataChanges;
}
}
void ForgetUnsavedNavMeshDataChanges(PrefabStage prefabStage)
{
// Debug.Log("On prefab closing - forget about this object's surfaces and stop caring about prefab saving");
if (prefabStage == null)
return;
var allSurfacesInPrefab = prefabStage.prefabContentsRoot.GetComponentsInChildren<NavMeshSurface>(true);
NavMeshSurface surfaceInPrefab = null;
var index = 0;
do
{
if (allSurfacesInPrefab.Length > 0)
surfaceInPrefab = allSurfacesInPrefab[index];
for (var i = m_PrefabNavMeshDataAssets.Count - 1; i >= 0; i--)
{
var storedPrefabInfo = m_PrefabNavMeshDataAssets[i];
if (storedPrefabInfo.surface == null)
{
// Debug.LogFormat("A surface from the prefab got deleted after it has baked a new NavMesh but it hasn't saved it. Now the unsaved asset gets deleted. ({0})", storedPrefabInfo.navMeshData);
// surface got deleted, thus delete its initial NavMeshData asset
if (storedPrefabInfo.navMeshData != null)
{
var assetPath = AssetDatabase.GetAssetPath(storedPrefabInfo.navMeshData);
AssetDatabase.DeleteAsset(assetPath);
}
m_PrefabNavMeshDataAssets.RemoveAt(i);
}
else if (surfaceInPrefab != null && storedPrefabInfo.surface == surfaceInPrefab)
{
//Debug.LogFormat("The surface {0} from the prefab was storing the original NavMesh data and now will be forgotten", surfaceInPrefab);
var baseSurface = PrefabUtility.GetCorrespondingObjectFromSource(surfaceInPrefab) as NavMeshSurface;
if (baseSurface == null || surfaceInPrefab.navMeshData != baseSurface.navMeshData)
{
var assetPath = AssetDatabase.GetAssetPath(surfaceInPrefab.navMeshData);
AssetDatabase.DeleteAsset(assetPath);
//Debug.LogFormat("The surface {0} from the prefab has baked new NavMeshData but did not save this change so the asset has been now deleted. ({1})",
// surfaceInPrefab, assetPath);
}
m_PrefabNavMeshDataAssets.RemoveAt(i);
}
}
} while (++index < allSurfacesInPrefab.Length);
if (m_PrefabNavMeshDataAssets.Count == 0)
{
PrefabStage.prefabSaving -= DeleteStoredNavMeshDataAssetsForOwnedSurfaces;
PrefabStage.prefabStageClosing -= ForgetUnsavedNavMeshDataChanges;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 178d8366aa1616849b91b66285c51454
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,355 @@
using System;
using UnityEditor;
using UnityEditor.AI;
using UnityEngine;
using UnityEngine.AI;
namespace Unity.AI.Navigation.Editor
{
/// <summary> Class containing a set of utility functions meant for presenting information from the NavMeshComponents into the GUI. </summary>
public static class NavMeshComponentsGUIUtility
{
static readonly GUIContent s_TempContent = new();
static GUIContent TempContent(string text)
{
s_TempContent.image = default;
s_TempContent.text = text;
s_TempContent.tooltip = default;
return s_TempContent;
}
internal const string k_PackageEditorResourcesFolder = "Packages/com.unity.ai.navigation/EditorResources/";
static readonly string k_OpenAreaSettingsText = L10n.Tr("Open Area Settings...");
/// <summary> Displays a GUI element for selecting the area type used by a <see cref="NavMeshSurface"/>, <see cref="NavMeshLink"/>, <see cref="NavMeshModifier"/> or <see cref="NavMeshModifierVolume"/>.</summary>
/// <remarks> The dropdown menu lists all of the area types defined in the <a href="../manual/NavigationWindow.html#areas-tab">Areas tab</a> of the Navigation window. </remarks>
/// <param name="labelName">The label for the field.</param>
/// <param name="areaProperty">The serialized property that this GUI element displays and modifies. It represents a NavMesh <see cref="NavMeshModifier.area">area type</see> and it needs to store values of type <see cref="SerializedPropertyType.Integer"/>.</param>
/// <seealso cref="NavMeshSurface.defaultArea">NavMeshSurface.defaultArea</seealso>
/// <seealso cref="NavMeshBuildSource.area">NavMeshBuildSource.area</seealso>
public static void AreaPopup(string labelName, SerializedProperty areaProperty) =>
AreaPopup(TempContent(labelName), areaProperty);
internal static void AreaPopup(GUIContent label, SerializedProperty areaProperty)
{
var areaIndex = -1;
var areaNames = GetNavMeshAreaNames();
for (var i = 0; i < areaNames.Length; i++)
{
var areaValue = GetNavMeshAreaFromName(areaNames[i]);
if (areaValue == areaProperty.intValue)
areaIndex = i;
}
ArrayUtility.Add(ref areaNames, "");
ArrayUtility.Add(ref areaNames, k_OpenAreaSettingsText);
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
EditorGUI.BeginProperty(rect, GUIContent.none, areaProperty);
EditorGUI.BeginChangeCheck();
areaIndex = EditorGUI.Popup(rect, label, areaIndex, areaNames);
if (EditorGUI.EndChangeCheck())
{
if (areaIndex >= 0 && areaIndex < areaNames.Length - 2)
areaProperty.intValue = GetNavMeshAreaFromName(areaNames[areaIndex]);
else if (areaIndex == areaNames.Length - 1)
NavMeshEditorHelpers.OpenAreaSettings();
}
EditorGUI.EndProperty();
}
internal static readonly string k_OpenAgentSettingsText = L10n.Tr("Open Agent Settings...");
static readonly string k_AgentTypeInvalidText = L10n.Tr("Agent Type invalid.");
/// <summary> Displays a GUI element for selecting the agent type used by a <see cref="NavMeshSurface"/> or <see cref="NavMeshLink"/>. </summary>
/// <remarks> The dropdown menu lists all of the agent types defined in the <a href="../manual/NavigationWindow.html#agents-tab">Agents tab</a> of the Navigation window. </remarks>
/// <param name="labelName">The label for the field.</param>
/// <param name="agentTypeID">The serialized property that this GUI element displays and modifies. It stores an <see cref="SerializedPropertyType.Integer"/> value that represents a NavMesh <see cref="NavMeshSurface.agentTypeID">agent type ID</see>.<br/>
/// The selected item is displayed as the <a href="https://docs.unity3d.com/ScriptReference/AI.NavMesh.GetSettingsNameFromID.html">name</a> that corresponds to the stored ID.</param>
/// <seealso href="https://docs.unity3d.com/ScriptReference/AI.NavMeshAgent-agentTypeID.html">NavMeshAgent.agentTypeID</seealso>
/// <seealso href="https://docs.unity3d.com/ScriptReference/AI.NavMeshBuildSettings-agentTypeID.html">NavMeshBuildSettings.agentTypeID</seealso>
/// <seealso href="https://docs.unity3d.com/ScriptReference/AI.NavMesh.GetSettingsNameFromID.html">NavMesh.GetSettingsNameFromID</seealso>
public static void AgentTypePopup(string labelName, SerializedProperty agentTypeID) =>
AgentTypePopup(TempContent(labelName), agentTypeID);
internal static void AgentTypePopup(GUIContent label, SerializedProperty agentTypeID)
{
var index = -1;
var count = NavMesh.GetSettingsCount();
var agentTypeNames = new string[count + 2];
for (var i = 0; i < count; i++)
{
var id = NavMesh.GetSettingsByIndex(i).agentTypeID;
var name = NavMesh.GetSettingsNameFromID(id);
agentTypeNames[i] = name;
if (id == agentTypeID.intValue)
index = i;
}
agentTypeNames[count] = "";
agentTypeNames[count + 1] = k_OpenAgentSettingsText;
bool validAgentType = index != -1;
if (!validAgentType)
{
EditorGUILayout.HelpBox(k_AgentTypeInvalidText, MessageType.Warning);
}
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
EditorGUI.BeginProperty(rect, GUIContent.none, agentTypeID);
EditorGUI.BeginChangeCheck();
index = EditorGUI.Popup(rect, label, index, agentTypeNames);
if (EditorGUI.EndChangeCheck())
{
if (index >= 0 && index < count)
{
var id = NavMesh.GetSettingsByIndex(index).agentTypeID;
agentTypeID.intValue = id;
}
else if (index == count + 1)
{
NavMeshEditorHelpers.OpenAgentSettings(-1);
}
}
EditorGUI.EndProperty();
}
// Agent mask is a set (internally array/list) of agentTypeIDs.
// It is used to describe which agents modifiers apply to.
// There is a special case of "None" which is an empty array.
// There is a special case of "All" which is an array of length 1, and value of -1.
/// <summary> Displays a GUI element for selecting multiple agent types for which a <see cref="NavMeshModifier"/> or <see cref="NavMeshModifierVolume"/> can influence the NavMesh.</summary>
/// <remarks> The dropdown menu lists all of the agent types defined in the <a href="../manual/NavigationWindow.html#agents-tab">Agents tab</a> of the Navigation window. </remarks>
/// <param name="labelName">The label for the field.</param>
/// <param name="agentMask">The serialized property that holds the <a href="https://docs.unity3d.com/ScriptReference/SerializedProperty-isArray.html">array</a> of NavMesh <see cref="NavMeshSurface.agentTypeID">agent type</see> values that are selected from the items defined in the <a href="../manual/NavigationWindow.html#agents-tab">Agents tab</a> of the Navigation window. The items are stored as <see cref="SerializedPropertyType.Integer"/> <see cref="NavMeshBuildSettings.agentTypeID">ID values</see> and are displayed as their corresponding <a href="https://docs.unity3d.com/ScriptReference/AI.NavMesh.GetSettingsNameFromID.html">names</a>.</param>
/// <seealso href="https://docs.unity3d.com/ScriptReference/AI.NavMesh.GetSettingsByIndex.html">NavMesh.GetSettingsByIndex</seealso>
/// <seealso href="Unity.AI.Navigation.NavMeshModifier.AffectsAgentType.html">NavMeshModifier.AffectsAgentType</seealso>
/// <seealso href="Unity.AI.Navigation.NavMeshModifierVolume.AffectsAgentType.html">NavMeshModifierVolume.AffectsAgentType</seealso>
public static void AgentMaskPopup(string labelName, SerializedProperty agentMask) =>
AgentMaskPopup(TempContent(labelName), agentMask);
internal static void AgentMaskPopup(GUIContent label, SerializedProperty agentMask)
{
// Contents of the dropdown box.
string popupContent = "";
if (agentMask.hasMultipleDifferentValues)
popupContent = "\u2014";
else
popupContent = GetAgentMaskLabelName(agentMask);
var content = TempContent(popupContent);
var popupRect = GUILayoutUtility.GetRect(content, EditorStyles.popup);
EditorGUI.BeginProperty(popupRect, GUIContent.none, agentMask);
popupRect = EditorGUI.PrefixLabel(popupRect, 0, label);
bool pressed = GUI.Button(popupRect, content, EditorStyles.popup);
if (pressed)
{
var show = !agentMask.hasMultipleDifferentValues;
var showNone = show && agentMask.arraySize == 0;
var showAll = show && IsAll(agentMask);
var menu = new GenericMenu();
menu.AddItem(new GUIContent("None"), showNone, SetAgentMaskNone, agentMask);
menu.AddItem(new GUIContent("All"), showAll, SetAgentMaskAll, agentMask);
menu.AddSeparator("");
var count = NavMesh.GetSettingsCount();
for (var i = 0; i < count; i++)
{
var id = NavMesh.GetSettingsByIndex(i).agentTypeID;
var sname = NavMesh.GetSettingsNameFromID(id);
var showSelected = show && AgentMaskHasSelectedAgentTypeID(agentMask, id);
var userData = new object[] { agentMask, id, !showSelected };
menu.AddItem(new GUIContent(sname), showSelected, ToggleAgentMaskItem, userData);
}
menu.DropDown(popupRect);
}
EditorGUI.EndProperty();
}
/// <summary> Creates and selects a new GameObject as a child of another GameObject. </summary>
/// <param name="suggestedName">The name given to the created child GameObject. If necessary, this method <a href="https://docs.unity3d.com/ScriptReference/GameObjectUtility.GetUniqueNameForSibling.html">modifies</a> the name in order to distinguish it from the other children of the same parent object.</param>
/// <param name="parent">The GameObject to which the created GameObject is attached as a child object.</param>
/// <returns>A new GameObject that is a child of the specified parent GameObject.</returns>
public static GameObject CreateAndSelectGameObject(string suggestedName, GameObject parent)
{
return CreateAndSelectGameObject(suggestedName, parent, Array.Empty<Type>());
}
internal static GameObject CreateAndSelectGameObject(string suggestedName, GameObject parent, params Type[] components)
{
var child = ObjectFactory.CreateGameObject(suggestedName, components);
GOCreationCommands.Place(child, parent);
return child;
}
/// <summary> Checks whether a serialized property has all the bits set when interpreted as a bitmask. </summary>
/// <param name="agentMask"></param>
/// <returns></returns>
static bool IsAll(SerializedProperty agentMask)
{
return agentMask.arraySize == 1 && agentMask.GetArrayElementAtIndex(0).intValue == -1;
}
/// <summary> Marks one agent type as being selected or not. </summary>
/// <param name="userData"></param>
static void ToggleAgentMaskItem(object userData)
{
var args = (object[])userData;
var agentMask = (SerializedProperty)args[0];
var agentTypeID = (int)args[1];
var value = (bool)args[2];
ToggleAgentMaskItem(agentMask, agentTypeID, value);
}
/// <summary> Marks one agent type as being selected or not. </summary>
/// <param name="agentMask"></param>
/// <param name="agentTypeID"></param>
/// <param name="value"></param>
static void ToggleAgentMaskItem(SerializedProperty agentMask, int agentTypeID, bool value)
{
if (agentMask.hasMultipleDifferentValues)
{
agentMask.ClearArray();
agentMask.serializedObject.ApplyModifiedProperties();
}
// Find which index this agent type is in the agentMask array.
int idx = -1;
for (var j = 0; j < agentMask.arraySize; j++)
{
var elem = agentMask.GetArrayElementAtIndex(j);
if (elem.intValue == agentTypeID)
idx = j;
}
// Handle "All" special case.
if (IsAll(agentMask))
{
agentMask.DeleteArrayElementAtIndex(0);
}
// Toggle value.
if (value)
{
if (idx == -1)
{
agentMask.InsertArrayElementAtIndex(agentMask.arraySize);
agentMask.GetArrayElementAtIndex(agentMask.arraySize - 1).intValue = agentTypeID;
}
}
else
{
if (idx != -1)
{
agentMask.DeleteArrayElementAtIndex(idx);
}
}
agentMask.serializedObject.ApplyModifiedProperties();
}
/// <summary> Marks all agent types as not being selected. </summary>
/// <param name="data"></param>
static void SetAgentMaskNone(object data)
{
var agentMask = (SerializedProperty)data;
agentMask.ClearArray();
agentMask.serializedObject.ApplyModifiedProperties();
}
/// <summary> Marks all agent types as being selected. </summary>
/// <param name="data"></param>
static void SetAgentMaskAll(object data)
{
var agentMask = (SerializedProperty)data;
agentMask.ClearArray();
agentMask.InsertArrayElementAtIndex(0);
agentMask.GetArrayElementAtIndex(0).intValue = -1;
agentMask.serializedObject.ApplyModifiedProperties();
}
static readonly string k_AgentMaskNoneText = L10n.Tr("None");
static readonly string k_AgentTypeAllText = L10n.Tr("All");
static readonly string k_AgentTypeMixedText = L10n.Tr("Mixed...");
/// <summary> Obtains one string that represents the current selection of agent types. </summary>
/// <param name="agentMask"></param>
/// <returns> One string that represents the current selection of agent types.</returns>
static string GetAgentMaskLabelName(SerializedProperty agentMask)
{
if (agentMask.arraySize == 0)
return k_AgentMaskNoneText;
if (IsAll(agentMask))
return k_AgentTypeAllText;
if (agentMask.arraySize <= 3)
{
var labelName = "";
for (var j = 0; j < agentMask.arraySize; j++)
{
var elem = agentMask.GetArrayElementAtIndex(j);
var settingsName = NavMesh.GetSettingsNameFromID(elem.intValue);
if (string.IsNullOrEmpty(settingsName))
continue;
if (labelName.Length > 0)
labelName += ", ";
labelName += settingsName;
}
return labelName;
}
return k_AgentTypeMixedText;
}
/// <summary> Checks whether a certain agent type is selected. </summary>
/// <param name="agentMask"></param>
/// <param name="agentTypeID"></param>
/// <returns></returns>
static bool AgentMaskHasSelectedAgentTypeID(SerializedProperty agentMask, int agentTypeID)
{
for (var j = 0; j < agentMask.arraySize; j++)
{
var elem = agentMask.GetArrayElementAtIndex(j);
if (elem.intValue == agentTypeID)
return true;
}
return false;
}
static string[] GetNavMeshAreaNames()
{
#if EDITOR_ONLY_NAVMESH_BUILDER_DEPRECATED
return NavMesh.GetAreaNames();
#else
return GameObjectUtility.GetNavMeshAreaNames();
#endif
}
static int GetNavMeshAreaFromName(string name)
{
#if EDITOR_ONLY_NAVMESH_BUILDER_DEPRECATED
return NavMesh.GetAreaFromName(name);
#else
return GameObjectUtility.GetNavMeshAreaFromName(name);
#endif
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 77fba670b979046f18d52d751e0d4659
timeCreated: 1480524815
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,418 @@
using System;
using UnityEditor;
using UnityEngine;
namespace Unity.AI.Navigation.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(NavMeshLink))]
class NavMeshLinkEditor : UnityEditor.Editor
{
SerializedProperty m_AgentTypeID;
SerializedProperty m_Area;
SerializedProperty m_IsOverridingCost;
SerializedProperty m_CostModifier;
SerializedProperty m_AutoUpdatePosition;
SerializedProperty m_Bidirectional;
SerializedProperty m_EndPoint;
SerializedProperty m_StartPoint;
SerializedProperty m_EndTransform;
SerializedProperty m_StartTransform;
SerializedProperty m_Activated;
SerializedProperty m_Width;
static int s_SelectedID;
static int s_SelectedPoint = -1;
static Color s_HandleColor = new Color(255f, 167f, 39f, 210f) / 255;
static Color s_HandleColorDisabled = new Color(255f * 0.75f, 167f * 0.75f, 39f * 0.75f, 100f) / 255;
void OnEnable()
{
m_AgentTypeID = serializedObject.FindProperty("m_AgentTypeID");
m_Area = serializedObject.FindProperty("m_Area");
m_IsOverridingCost = serializedObject.FindProperty("m_IsOverridingCost");
m_CostModifier = serializedObject.FindProperty("m_CostModifier");
m_AutoUpdatePosition = serializedObject.FindProperty("m_AutoUpdatePosition");
m_Bidirectional = serializedObject.FindProperty("m_Bidirectional");
m_EndPoint = serializedObject.FindProperty("m_EndPoint");
m_StartPoint = serializedObject.FindProperty("m_StartPoint");
m_EndTransform = serializedObject.FindProperty("m_EndTransform");
m_StartTransform = serializedObject.FindProperty("m_StartTransform");
m_Activated = serializedObject.FindProperty("m_Activated");
m_Width = serializedObject.FindProperty("m_Width");
s_SelectedID = 0;
s_SelectedPoint = -1;
}
internal static void AlignTransformToEndPoints(NavMeshLink navLink)
{
var toWorld = navLink.LocalToWorldUnscaled();
var allLinksOnGameObject = navLink.gameObject.GetComponents<NavMeshLink>();
Span<(Vector3 start, Vector3 end)> serializedEndpointsWorld = stackalloc (Vector3, Vector3)[allLinksOnGameObject.Length];
Span<(Vector3 start, Vector3 end)> worldEndpointsBeforeAlign = stackalloc (Vector3, Vector3)[allLinksOnGameObject.Length];
var thisLink = -1;
for (var i = 0; i < allLinksOnGameObject.Length; i++)
{
var link = allLinksOnGameObject[i];
if (link == navLink)
thisLink = i;
// Store the world positions of the serialized point values
serializedEndpointsWorld[i].start = toWorld.MultiplyPoint3x4(link.startPoint);
serializedEndpointsWorld[i].end = toWorld.MultiplyPoint3x4(link.endPoint);
// Store the world positions of the used endpoints
link.GetWorldPositions(out var initialStartPos, out var initialEndPos);
worldEndpointsBeforeAlign[i].start = initialStartPos;
worldEndpointsBeforeAlign[i].end = initialEndPos;
}
// Use overall world position of target points or transforms to determine midpoint
var worldStartPt = worldEndpointsBeforeAlign[thisLink].start;
var worldEndPt = worldEndpointsBeforeAlign[thisLink].end;
var startToEnd = worldEndPt - worldStartPt;
var up = navLink.transform.up;
// Flatten
var forward = startToEnd - Vector3.Dot(up, startToEnd) * up;
Undo.RecordObject(navLink.transform, Content.UndoReCenterOrigin);
var middlePos = (worldEndPt + worldStartPt) * 0.5f;
var lookTowardsEndAndKeepUp = Quaternion.LookRotation(forward, up);
navLink.transform.SetPositionAndRotation(middlePos, lookTowardsEndAndKeepUp);
// Transform points back to local space
var toNewLocal = navLink.LocalToWorldUnscaled().inverse;
for (var i = 0; i < allLinksOnGameObject.Length; i++)
{
var link = allLinksOnGameObject[i];
Undo.RecordObject(link, Content.UndoReCenterOrigin);
var startAtOwnChild = link.startTransform != null && link.startTransform.IsChildOf(link.transform);
var endAtOwnChild = link.endTransform != null && link.endTransform.IsChildOf(link.transform);
if (startAtOwnChild)
Undo.RecordObject(link.startTransform, Content.UndoReCenterOrigin);
if (endAtOwnChild)
Undo.RecordObject(link.endTransform, Content.UndoReCenterOrigin);
using (new DeferredLinkUpdateScope(link))
{
link.startPoint = toNewLocal.MultiplyPoint3x4(serializedEndpointsWorld[i].start);
link.endPoint = toNewLocal.MultiplyPoint3x4(serializedEndpointsWorld[i].end);
// Ensure transform targets return to world positions, in case they are children of the NavMeshLink object
if (startAtOwnChild)
link.startTransform.position = worldEndpointsBeforeAlign[i].start;
if (endAtOwnChild)
link.endTransform.position = worldEndpointsBeforeAlign[i].end;
}
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
NavMeshComponentsGUIUtility.AgentTypePopup(Content.AgentType, m_AgentTypeID);
NavMeshComponentsGUIUtility.AreaPopup(Content.AreaType, m_Area);
EditorGUILayout.PropertyField(m_IsOverridingCost, Content.CostOverrideToggle);
EditorGUI.BeginDisabled(!m_IsOverridingCost.boolValue);
EditorGUILayout.PropertyField(m_CostModifier, Content.CostModifier);
EditorGUI.EndDisabled();
EditorGUILayout.Space();
m_StartPoint.isExpanded = EditorGUILayout.Foldout(m_StartPoint.isExpanded, Content.Positions, true);
if (m_StartPoint.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_StartTransform, Content.StartTransform);
using (new EditorGUI.DisabledScope(m_StartTransform.objectReferenceValue != null))
EditorGUILayout.PropertyField(m_StartPoint, Content.StartPoint);
EditorGUILayout.PropertyField(m_EndTransform, Content.EndTransform);
using (new EditorGUI.DisabledScope(m_EndTransform.objectReferenceValue != null))
EditorGUILayout.PropertyField(m_EndPoint, Content.EndPoint);
var buttonRect = EditorGUILayout.GetControlRect();
buttonRect = EditorGUI.IndentedRect(buttonRect);
buttonRect.width -= EditorGUIUtility.standardVerticalSpacing;
buttonRect.width *= 0.5f;
if (GUI.Button(buttonRect, Content.ReverseDirectionButton))
{
Undo.RecordObjects(targets, Content.UndoReverseDirection);
foreach (var nml in targets)
ReverseDirection((NavMeshLink)nml);
SceneView.RepaintAll();
}
buttonRect.x += buttonRect.width + EditorGUIUtility.standardVerticalSpacing;
if (GUI.Button(buttonRect, Content.ReCenterButton))
{
Undo.RecordObjects(targets, Content.UndoReCenterOrigin);
foreach (var nml in targets)
AlignTransformToEndPoints((NavMeshLink)nml);
SceneView.RepaintAll();
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_AutoUpdatePosition, Content.AutoUpdatePositions);
EditorGUILayout.PropertyField(m_Bidirectional, Content.Bidirectional);
EditorGUILayout.PropertyField(m_Width, Content.Width);
EditorGUILayout.PropertyField(m_Activated, Content.Activated);
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space();
}
internal static void ReverseDirection(NavMeshLink navLink)
{
using (new DeferredLinkUpdateScope(navLink))
{
(navLink.startPoint, navLink.endPoint) = (navLink.endPoint, navLink.startPoint);
(navLink.startTransform, navLink.endTransform) = (navLink.endTransform, navLink.startTransform);
}
}
internal static Vector3 GetLocalDirectionRight(NavMeshLink navLink, out Vector3 localStartPosition, out Vector3 localEndPosition)
{
navLink.GetLocalPositions(out localStartPosition, out localEndPosition);
var dir = localEndPosition - localStartPosition;
return new Vector3(dir.z, 0.0f, -dir.x).normalized;
}
static void DrawLink(NavMeshLink navLink)
{
var right = GetLocalDirectionRight(navLink, out var startPos, out var endPos);
var rad = navLink.width * 0.5f;
var edgeRadius = right * rad;
ReadOnlySpan<Vector3> corners = stackalloc[]
{
startPos - edgeRadius,
startPos + edgeRadius,
endPos + edgeRadius,
endPos - edgeRadius
};
Gizmos.DrawLineStrip(corners, true);
}
[DrawGizmo(GizmoType.InSelectionHierarchy | GizmoType.Active | GizmoType.Pickable)]
static void RenderBoxGizmo(NavMeshLink navLink, GizmoType gizmoType)
{
if (!EditorApplication.isPlaying && navLink.isActiveAndEnabled && navLink.HaveTransformsChanged())
navLink.UpdateLink();
var color = s_HandleColor;
if (!navLink.enabled)
color = s_HandleColorDisabled;
var oldColor = Gizmos.color;
var oldMatrix = Gizmos.matrix;
Gizmos.matrix = navLink.LocalToWorldUnscaled();
Gizmos.color = color;
DrawLink(navLink);
Gizmos.matrix = oldMatrix;
Gizmos.color = oldColor;
Gizmos.DrawIcon(navLink.transform.position, "NavMeshLink Icon", true);
}
[DrawGizmo(GizmoType.NotInSelectionHierarchy | GizmoType.Pickable)]
static void RenderBoxGizmoNotSelected(NavMeshLink navLink, GizmoType gizmoType)
{
if (!EditorApplication.isPlaying && navLink.isActiveAndEnabled && navLink.HaveTransformsChanged())
navLink.UpdateLink();
var color = s_HandleColor;
if (!navLink.enabled)
color = s_HandleColorDisabled;
var oldColor = Gizmos.color;
var oldMatrix = Gizmos.matrix;
Gizmos.matrix = navLink.LocalToWorldUnscaled();
Gizmos.color = color;
DrawLink(navLink);
Gizmos.matrix = oldMatrix;
Gizmos.color = oldColor;
Gizmos.DrawIcon(navLink.transform.position, "NavMeshLink Icon", true);
}
public void OnSceneGUI()
{
var navLink = (NavMeshLink)target;
if (!navLink.enabled)
return;
var toWorld = navLink.LocalToWorldUnscaled();
navLink.GetWorldPositions(out var worldStartPt, out var worldEndPt);
var worldMidPt = Vector3.Lerp(worldStartPt, worldEndPt, 0.35f);
var startSize = HandleUtility.GetHandleSize(worldStartPt);
var endSize = HandleUtility.GetHandleSize(worldEndPt);
var midSize = HandleUtility.GetHandleSize(worldMidPt);
var zup = Quaternion.FromToRotation(Vector3.forward, Vector3.up);
var right = toWorld.MultiplyVector(GetLocalDirectionRight(navLink, out _, out _));
var oldColor = Handles.color;
Handles.color = s_HandleColor;
Vector3 newWorldPos;
var startIsLocal = navLink.startTransform == null;
if (s_SelectedPoint == 0 && navLink.GetInstanceID() == s_SelectedID)
{
EditorGUI.BeginChangeCheck();
if (startIsLocal)
Handles.CubeHandleCap(0, worldStartPt, zup, 0.1f * startSize, Event.current.type);
else
Handles.SphereHandleCap(0, worldStartPt, zup, 0.1f * startSize, Event.current.type);
newWorldPos = Handles.PositionHandle(worldStartPt, navLink.transform.rotation);
if (EditorGUI.EndChangeCheck())
{
if (startIsLocal)
{
Undo.RecordObject(navLink, Content.UndoMoveLinkStartPoint);
navLink.startPoint = toWorld.inverse.MultiplyPoint3x4(newWorldPos);
}
else
{
Undo.RecordObject(navLink.startTransform, Content.UndoMoveLinkStartObject);
navLink.startTransform.position = newWorldPos;
}
}
}
else
{
if (Handles.Button(worldStartPt, zup, 0.1f * startSize, 0.1f * startSize,
startIsLocal ? Handles.CubeHandleCap : Handles.SphereHandleCap))
{
s_SelectedPoint = 0;
s_SelectedID = navLink.GetInstanceID();
}
}
var endIsLocal = navLink.endTransform == null;
if (s_SelectedPoint == 1 && navLink.GetInstanceID() == s_SelectedID)
{
EditorGUI.BeginChangeCheck();
if (endIsLocal)
Handles.CubeHandleCap(0, worldEndPt, zup, 0.1f * endSize, Event.current.type);
else
Handles.SphereHandleCap(0, worldEndPt, zup, 0.1f * endSize, Event.current.type);
newWorldPos = Handles.PositionHandle(worldEndPt, navLink.transform.rotation);
if (EditorGUI.EndChangeCheck())
{
if (endIsLocal)
{
Undo.RecordObject(navLink, Content.UndoMoveLinkEndPoint);
navLink.endPoint = toWorld.inverse.MultiplyPoint3x4(newWorldPos);
}
else
{
Undo.RecordObject(navLink.endTransform, Content.UndoMoveLinkEndObject);
navLink.endTransform.position = newWorldPos;
}
}
}
else
{
if (Handles.Button(worldEndPt, zup, 0.1f * endSize, 0.1f * endSize,
endIsLocal ? Handles.CubeHandleCap : Handles.SphereHandleCap))
{
s_SelectedPoint = 1;
s_SelectedID = navLink.GetInstanceID();
}
}
EditorGUI.BeginChangeCheck();
newWorldPos = Handles.Slider(worldMidPt + 0.5f * navLink.width * right, right, midSize * 0.03f, Handles.DotHandleCap, 0);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(navLink, Content.UndoModifyLinkWidth);
navLink.width = Mathf.Max(0.0f, 2.0f * Vector3.Dot(right, (newWorldPos - worldMidPt)));
}
EditorGUI.BeginChangeCheck();
newWorldPos = Handles.Slider(worldMidPt - 0.5f * navLink.width * right, -right, midSize * 0.03f, Handles.DotHandleCap, 0);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(navLink, Content.UndoModifyLinkWidth);
navLink.width = Mathf.Max(0.0f, 2.0f * Vector3.Dot(-right, (newWorldPos - worldMidPt)));
}
Handles.color = oldColor;
}
[MenuItem("GameObject/AI/NavMesh Link", false, 2002)]
public static void CreateNavMeshLink(MenuCommand menuCommand)
{
var parent = menuCommand.context as GameObject;
NavMeshComponentsGUIUtility.CreateAndSelectGameObject("NavMesh Link", parent, typeof(NavMeshLink));
}
internal class DeferredLinkUpdateScope : IDisposable
{
readonly NavMeshLink m_NavLink;
readonly bool m_WasEnabled;
public DeferredLinkUpdateScope(NavMeshLink link)
{
m_NavLink = link;
m_WasEnabled = link.enabled;
link.enabled = false;
}
public void Dispose()
{
m_NavLink.enabled = m_WasEnabled;
}
}
static class Content
{
public static readonly GUIContent AgentType = EditorGUIUtility.TrTextContent("Agent Type", "Specifies the agent type that can use the link.");
public static readonly GUIContent AreaType = EditorGUIUtility.TrTextContent("Area Type", "The area type of the NavMesh Link, which affects pathfinding costs.");
public static readonly GUIContent CostOverrideToggle = EditorGUIUtility.TrTextContent("Cost Override", "If enabled, the value below will be used instead of the area cost defined in the Navigation window.");
public static readonly GUIContent CostModifier = EditorGUIUtility.TrTextContent(" ", "If Cost Override is enabled, the link uses this cost instead of the area cost defined in the Navigation window.");
public static readonly GUIContent Positions = EditorGUIUtility.TrTextContent("Positions", "Configure the ends of the link, each specified either as a Transform's position or as an unscaled offset in the space of this GameObject.");
public static readonly GUIContent StartTransform = EditorGUIUtility.TrTextContent("Start Transform", "Transform whose world position specifies the link start point.");
public static readonly GUIContent StartPoint = EditorGUIUtility.TrTextContent("Start Point", "A local position where the link starts. Used only if Start Transform does not reference any object.");
public static readonly GUIContent EndTransform = EditorGUIUtility.TrTextContent("End Transform", "Transform whose world position specifies the link end point.");
public static readonly GUIContent EndPoint = EditorGUIUtility.TrTextContent("End Point", "A local position where the link ends. Used only if End Transform does not reference any object.");
public static readonly GUIContent ReverseDirectionButton = EditorGUIUtility.TrTextContent("Swap", "Reverse the direction of the link by swapping the start and end.");
public static readonly GUIContent ReCenterButton = EditorGUIUtility.TrTextContent("Re-Center Origin", "Place this GameObject at the middle point between the start and end of this link, and rotate it to point forward from the link start towards the end.");
public static readonly GUIContent Width = EditorGUIUtility.TrTextContent("Width", "World-space width of the segments making up the ends of the link.");
public static readonly GUIContent AutoUpdatePositions = EditorGUIUtility.TrTextContent("Auto Update Positions", "If enabled, the link will automatically update when the Start Transform, End Transform, or this GameObject's Transform changes.");
public static readonly GUIContent Bidirectional = EditorGUIUtility.TrTextContent("Bidirectional", "If enabled, agents can traverse the link in both directions.");
public static readonly GUIContent Activated = EditorGUIUtility.TrTextContent("Activated", "If enabled, allows the agents to traverse the link.");
public static readonly string UndoReCenterOrigin = L10n.Tr("Re-Center NavMesh Link origin");
public static readonly string UndoReverseDirection = L10n.Tr("Swap NavMesh Link start and end");
public static readonly string UndoMoveLinkStartPoint = L10n.Tr("Move NavMesh Link start point");
public static readonly string UndoMoveLinkStartObject = L10n.Tr("Move NavMesh Link start object");
public static readonly string UndoMoveLinkEndPoint = L10n.Tr("Move NavMesh Link end point");
public static readonly string UndoMoveLinkEndObject = L10n.Tr("Move NavMesh Link end object");
public static readonly string UndoModifyLinkWidth = L10n.Tr("Modify NavMesh Link width");
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ece1e865d1ad84587872fe8580ab5a20
timeCreated: 1477036743
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using UnityEditor;
using UnityEngine;
namespace Unity.AI.Navigation.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(NavMeshModifier))]
class NavMeshModifierEditor : UnityEditor.Editor
{
SerializedProperty m_AffectedAgents;
SerializedProperty m_IgnoreFromBuild;
SerializedProperty m_OverrideArea;
SerializedProperty m_Area;
SerializedProperty m_ApplyToChildren;
SerializedProperty m_OverrideGenerateLinks;
SerializedProperty m_GenerateLinks;
void OnEnable()
{
m_AffectedAgents = serializedObject.FindProperty("m_AffectedAgents");
m_IgnoreFromBuild = serializedObject.FindProperty("m_IgnoreFromBuild");
m_OverrideArea = serializedObject.FindProperty("m_OverrideArea");
m_Area = serializedObject.FindProperty("m_Area");
m_ApplyToChildren = serializedObject.FindProperty("m_ApplyToChildren");
m_OverrideGenerateLinks = serializedObject.FindProperty("m_OverrideGenerateLinks");
m_GenerateLinks = serializedObject.FindProperty("m_GenerateLinks");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
int mode = m_IgnoreFromBuild.boolValue ? 1 : 0;
mode = EditorGUILayout.Popup(Content.Mode, mode, Content.ModeChoices);
m_IgnoreFromBuild.boolValue = mode == 1;
NavMeshComponentsGUIUtility.AgentMaskPopup(Content.AffectedAgents, m_AffectedAgents);
EditorGUILayout.PropertyField(m_ApplyToChildren, Content.ApplyToChildren);
if (!m_IgnoreFromBuild.boolValue)
{
EditorGUILayout.PropertyField(m_OverrideArea, Content.OverrideArea);
if (m_OverrideArea.boolValue)
{
EditorGUI.indentLevel++;
NavMeshComponentsGUIUtility.AreaPopup(Content.AreaType, m_Area);
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(m_OverrideGenerateLinks, Content.OverrideGenerateLinks);
if (m_OverrideGenerateLinks.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_GenerateLinks, Content.GenerateLinks);
EditorGUI.indentLevel--;
}
}
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
}
static class Content
{
public static readonly GUIContent Mode = EditorGUIUtility.TrTextContent("Mode", "Specifies whether to consider or ignore the affected GameObject(s).");
public static readonly string[] ModeChoices = { L10n.Tr("Add or Modify Object"), L10n.Tr("Remove Object") };
public static readonly GUIContent AffectedAgents = EditorGUIUtility.TrTextContent("Affected Agents", "Specifies which agents the NavMesh Modifier affects.");
public static readonly GUIContent ApplyToChildren = EditorGUIUtility.TrTextContent("Apply To Children", "If enabled, applies the configuration to the child hierarchy of the GameObject until another NavMesh Modifier component is encountered.");
public static readonly GUIContent OverrideArea = EditorGUIUtility.TrTextContent("Override Area", "If enabled, the area type of the NavMeshModifier will be overridden by the area type selected below.");
public static readonly GUIContent AreaType = EditorGUIUtility.TrTextContent("Area Type", "The area type of the NavMeshModifier.");
public static readonly GUIContent OverrideGenerateLinks = EditorGUIUtility.TrTextContent("Override Generate Links", "If enabled, forces the NavMesh bake process to either include or ignore the affected GameObject(s) when you generate links.");
public static readonly GUIContent GenerateLinks = EditorGUIUtility.TrTextContent("Generate Links", "If enabled, specifies whether or not to include the affected GameObject(s) when you generate links.");
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6fa04b4743e3947eba4d7b9e5832ea69
timeCreated: 1477036742
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,141 @@
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
namespace Unity.AI.Navigation.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(NavMeshModifierVolume))]
class NavMeshModifierVolumeEditor : UnityEditor.Editor
{
SerializedProperty m_AffectedAgents;
SerializedProperty m_Area;
SerializedProperty m_Center;
SerializedProperty m_Size;
static Color s_HandleColor = new Color(187f, 138f, 240f, 210f) / 255;
static Color s_HandleColorDisabled = new Color(187f * 0.75f, 138f * 0.75f, 240f * 0.75f, 100f) / 255;
BoxBoundsHandle m_BoundsHandle = new BoxBoundsHandle();
bool editingCollider
{
get { return EditMode.editMode == EditMode.SceneViewEditMode.Collider && EditMode.IsOwner(this); }
}
void OnEnable()
{
m_AffectedAgents = serializedObject.FindProperty("m_AffectedAgents");
m_Area = serializedObject.FindProperty("m_Area");
m_Center = serializedObject.FindProperty("m_Center");
m_Size = serializedObject.FindProperty("m_Size");
}
Bounds GetBounds()
{
var navModifier = (NavMeshModifierVolume)target;
return new Bounds(navModifier.transform.position, navModifier.size);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditMode.DoEditModeInspectorModeButton(EditMode.SceneViewEditMode.Collider, "Edit Volume",
EditorGUIUtility.IconContent("EditCollider"), GetBounds, this);
EditorGUILayout.PropertyField(m_Size, Content.Size);
EditorGUILayout.PropertyField(m_Center, Content.Center);
NavMeshComponentsGUIUtility.AreaPopup(Content.Area, m_Area);
NavMeshComponentsGUIUtility.AgentMaskPopup(Content.AffectedAgents, m_AffectedAgents);
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
}
[DrawGizmo(GizmoType.InSelectionHierarchy | GizmoType.Active)]
static void RenderBoxGizmo(NavMeshModifierVolume navModifier, GizmoType gizmoType)
{
var color = navModifier.enabled ? s_HandleColor : s_HandleColorDisabled;
var colorTrans = new Color(color.r * 0.75f, color.g * 0.75f, color.b * 0.75f, color.a * 0.15f);
var oldColor = Gizmos.color;
var oldMatrix = Gizmos.matrix;
Gizmos.matrix = navModifier.transform.localToWorldMatrix;
Gizmos.color = colorTrans;
Gizmos.DrawCube(navModifier.center, navModifier.size);
Gizmos.color = color;
Gizmos.DrawWireCube(navModifier.center, navModifier.size);
Gizmos.matrix = oldMatrix;
Gizmos.color = oldColor;
Gizmos.DrawIcon(navModifier.transform.position, "NavMeshModifierVolume Icon", true);
}
[DrawGizmo(GizmoType.NotInSelectionHierarchy | GizmoType.Pickable)]
static void RenderBoxGizmoNotSelected(NavMeshModifierVolume navModifier, GizmoType gizmoType)
{
var color = navModifier.enabled ? s_HandleColor : s_HandleColorDisabled;
var oldColor = Gizmos.color;
var oldMatrix = Gizmos.matrix;
Gizmos.matrix = navModifier.transform.localToWorldMatrix;
Gizmos.color = color;
Gizmos.DrawWireCube(navModifier.center, navModifier.size);
Gizmos.matrix = oldMatrix;
Gizmos.color = oldColor;
Gizmos.DrawIcon(navModifier.transform.position, "NavMeshModifierVolume Icon", true);
}
void OnSceneGUI()
{
if (!editingCollider)
return;
var vol = (NavMeshModifierVolume)target;
var color = vol.enabled ? s_HandleColor : s_HandleColorDisabled;
using (new Handles.DrawingScope(color, vol.transform.localToWorldMatrix))
{
m_BoundsHandle.center = vol.center;
m_BoundsHandle.size = vol.size;
EditorGUI.BeginChangeCheck();
m_BoundsHandle.DrawHandle();
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(vol, Content.UndoModifyVolume);
Vector3 center = m_BoundsHandle.center;
Vector3 size = m_BoundsHandle.size;
vol.center = center;
vol.size = size;
EditorUtility.SetDirty(target);
}
}
}
[MenuItem("GameObject/AI/NavMesh Modifier Volume", false, 2001)]
public static void CreateNavMeshModifierVolume(MenuCommand menuCommand)
{
var parent = menuCommand.context as GameObject;
NavMeshComponentsGUIUtility.CreateAndSelectGameObject("NavMesh Modifier Volume", parent, typeof(NavMeshModifierVolume));
}
static class Content
{
public static readonly GUIContent Size = EditorGUIUtility.TrTextContent("Size", "Dimensions of the NavMesh Modifier Volume.");
public static readonly GUIContent Center = EditorGUIUtility.TrTextContent("Center", "The center of the NavMesh Modifier Volume relative to the GameObject center.");
public static readonly GUIContent Area = EditorGUIUtility.TrTextContent("Area Type", "Describes the area type to which the NavMesh Modifier Volume applies.");
public static readonly GUIContent AffectedAgents = EditorGUIUtility.TrTextContent("Affected Agents", "A selection of agent types that the NavMesh Modifier Volume affects.");
public static readonly string UndoModifyVolume = L10n.Tr("Modify NavMesh Modifier Volume");
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: c0f3bef2a67ae4e139538afec3e59b03
timeCreated: 1477036743
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,406 @@
#define NAVMESHCOMPONENTS_SHOW_NAVMESHDATA_REF
using UnityEditor;
using UnityEditor.AI;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine.AI;
using UnityEngine;
using System.Linq;
namespace Unity.AI.Navigation.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(NavMeshSurface))]
class NavMeshSurfaceEditor : UnityEditor.Editor
{
SerializedProperty m_AgentTypeID;
SerializedProperty m_BuildHeightMesh;
SerializedProperty m_Center;
SerializedProperty m_CollectObjects;
SerializedProperty m_DefaultArea;
SerializedProperty m_GenerateLinks;
SerializedProperty m_LayerMask;
SerializedProperty m_OverrideTileSize;
SerializedProperty m_OverrideVoxelSize;
SerializedProperty m_Size;
SerializedProperty m_TileSize;
SerializedProperty m_UseGeometry;
SerializedProperty m_VoxelSize;
SerializedProperty m_MinRegionArea;
SerializedProperty m_LedgeDropHeight;
SerializedProperty m_MaxJumpAcrossDistance;
#if NAVMESHCOMPONENTS_SHOW_NAVMESHDATA_REF
SerializedProperty m_NavMeshData;
#endif
static bool s_ShowDebugOptions;
static Color s_HandleColor = new Color(127f, 214f, 244f, 100f) / 255;
static Color s_HandleColorSelected = new Color(127f, 214f, 244f, 210f) / 255;
static Color s_HandleColorDisabled = new Color(127f * 0.75f, 214f * 0.75f, 244f * 0.75f, 100f) / 255;
BoxBoundsHandle m_BoundsHandle = new BoxBoundsHandle();
bool editingCollider
{
get { return EditMode.editMode == EditMode.SceneViewEditMode.Collider && EditMode.IsOwner(this); }
}
void OnEnable()
{
m_AgentTypeID = serializedObject.FindProperty("m_AgentTypeID");
m_BuildHeightMesh = serializedObject.FindProperty("m_BuildHeightMesh");
m_Center = serializedObject.FindProperty("m_Center");
m_CollectObjects = serializedObject.FindProperty("m_CollectObjects");
m_DefaultArea = serializedObject.FindProperty("m_DefaultArea");
m_GenerateLinks = serializedObject.FindProperty("m_GenerateLinks");
m_LayerMask = serializedObject.FindProperty("m_LayerMask");
m_OverrideTileSize = serializedObject.FindProperty("m_OverrideTileSize");
m_OverrideVoxelSize = serializedObject.FindProperty("m_OverrideVoxelSize");
m_Size = serializedObject.FindProperty("m_Size");
m_TileSize = serializedObject.FindProperty("m_TileSize");
m_UseGeometry = serializedObject.FindProperty("m_UseGeometry");
m_VoxelSize = serializedObject.FindProperty("m_VoxelSize");
m_MinRegionArea = serializedObject.FindProperty("m_MinRegionArea");
#if NAVMESHCOMPONENTS_SHOW_NAVMESHDATA_REF
m_NavMeshData = serializedObject.FindProperty("m_NavMeshData");
#endif
}
Bounds GetBounds()
{
var navSurface = (NavMeshSurface)target;
return new Bounds(navSurface.transform.position, navSurface.size);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
var bs = NavMesh.GetSettingsByID(m_AgentTypeID.intValue);
if (bs.agentTypeID != -1)
{
// Draw image
const float diagramHeight = 80.0f;
Rect agentDiagramRect = EditorGUILayout.GetControlRect(false, diagramHeight);
NavMeshEditorHelpers.DrawAgentDiagram(agentDiagramRect, bs.agentRadius, bs.agentHeight, bs.agentClimb, bs.agentSlope);
}
NavMeshComponentsGUIUtility.AgentTypePopup(Content.AgentType, m_AgentTypeID);
NavMeshComponentsGUIUtility.AreaPopup(Content.DefaultArea, m_DefaultArea);
EditorGUILayout.PropertyField(m_GenerateLinks, Content.GenerateLinks);
EditorGUILayout.PropertyField(m_UseGeometry, Content.UseGeometry);
m_CollectObjects.isExpanded = EditorGUILayout.Foldout(m_CollectObjects.isExpanded, Content.ObjectCollectionHeader, true);
if (m_CollectObjects.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_CollectObjects, Content.CollectObjects);
if ((CollectObjects)m_CollectObjects.enumValueIndex == CollectObjects.Volume)
{
EditorGUI.indentLevel++;
EditMode.DoEditModeInspectorModeButton(EditMode.SceneViewEditMode.Collider, "Edit Volume",
EditorGUIUtility.IconContent("EditCollider"), GetBounds, this);
EditorGUILayout.PropertyField(m_Size);
EditorGUILayout.PropertyField(m_Center);
EditorGUI.indentLevel--;
}
else
{
if (editingCollider)
EditMode.QuitEditMode();
}
EditorGUILayout.PropertyField(m_LayerMask, Content.IncludeLayers);
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
m_OverrideVoxelSize.isExpanded = EditorGUILayout.Foldout(m_OverrideVoxelSize.isExpanded, Content.AdvancedHeader, true);
if (m_OverrideVoxelSize.isExpanded)
{
EditorGUI.indentLevel++;
// Override voxel size.
EditorGUILayout.PropertyField(m_OverrideVoxelSize, Content.OverrideVoxelSize);
using (new EditorGUI.DisabledScope(!m_OverrideVoxelSize.boolValue || m_OverrideVoxelSize.hasMultipleDifferentValues))
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_VoxelSize, Content.VoxelSize);
if (!m_OverrideVoxelSize.hasMultipleDifferentValues)
{
if (!m_AgentTypeID.hasMultipleDifferentValues)
{
float voxelsPerRadius = m_VoxelSize.floatValue > 0.0f ? (bs.agentRadius / m_VoxelSize.floatValue) : 0.0f;
EditorGUILayout.LabelField(" ", string.Format(Content.VoxelSizeFormatString, voxelsPerRadius), EditorStyles.miniLabel);
}
if (m_OverrideVoxelSize.boolValue)
EditorGUILayout.HelpBox(Content.VoxelSizeHelpBox, MessageType.None);
}
EditorGUI.indentLevel--;
}
// Override tile size
EditorGUILayout.PropertyField(m_OverrideTileSize, Content.OverrideTileSize);
using (new EditorGUI.DisabledScope(!m_OverrideTileSize.boolValue || m_OverrideTileSize.hasMultipleDifferentValues))
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_TileSize, Content.TileSize);
if (!m_TileSize.hasMultipleDifferentValues && !m_VoxelSize.hasMultipleDifferentValues)
{
float tileWorldSize = m_TileSize.intValue * m_VoxelSize.floatValue;
EditorGUILayout.LabelField(" ", string.Format(Content.TileWorldSizeFormatString, tileWorldSize), EditorStyles.miniLabel);
}
if (!m_OverrideTileSize.hasMultipleDifferentValues)
{
if (m_OverrideTileSize.boolValue)
EditorGUILayout.HelpBox(Content.TileWorldSizeHelpBox, MessageType.None);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(m_MinRegionArea, Content.MinimumRegionArea);
// Height mesh
EditorGUILayout.PropertyField(m_BuildHeightMesh, Content.BuildHeightMesh);
EditorGUILayout.Space();
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
var hadError = false;
var multipleTargets = targets.Length > 1;
foreach (NavMeshSurface navSurface in targets)
{
var settings = navSurface.GetBuildSettings();
// Calculating bounds is potentially expensive when unbounded - so here we just use the center/size.
// It means the validation is not checking vertical voxel limit correctly when the surface is set to something else than "in volume".
var bounds = new Bounds(Vector3.zero, Vector3.zero);
if (navSurface.collectObjects == CollectObjects.Volume)
{
bounds = new Bounds(navSurface.center, navSurface.size);
}
var errors = settings.ValidationReport(bounds);
if (errors.Length > 0)
{
if (multipleTargets)
EditorGUILayout.LabelField(navSurface.name);
foreach (var err in errors)
{
EditorGUILayout.HelpBox(err, MessageType.Warning);
}
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUIUtility.labelWidth);
if (GUILayout.Button(NavMeshComponentsGUIUtility.k_OpenAgentSettingsText, EditorStyles.miniButton))
NavMeshEditorHelpers.OpenAgentSettings(navSurface.agentTypeID);
GUILayout.EndHorizontal();
hadError = true;
}
}
if (hadError)
EditorGUILayout.Space();
#if NAVMESHCOMPONENTS_SHOW_NAVMESHDATA_REF
var nmdRect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
EditorGUI.BeginProperty(nmdRect, GUIContent.none, m_NavMeshData);
var rectLabel = EditorGUI.PrefixLabel(nmdRect, GUIUtility.GetControlID(FocusType.Passive), Content.NavMeshData);
EditorGUI.EndProperty();
using (new EditorGUI.DisabledScope(true))
{
EditorGUI.BeginProperty(nmdRect, GUIContent.none, m_NavMeshData);
EditorGUI.ObjectField(rectLabel, m_NavMeshData, GUIContent.none);
EditorGUI.EndProperty();
}
#endif
using (new EditorGUI.DisabledScope(Application.isPlaying || m_AgentTypeID.intValue == -1))
{
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUIUtility.labelWidth);
using (new EditorGUI.DisabledScope(targets.All(s => (s as NavMeshSurface)?.navMeshData == null)))
{
if (GUILayout.Button(Content.ClearButton))
{
NavMeshAssetManager.instance.ClearSurfaces(targets);
SceneView.RepaintAll();
}
}
if (GUILayout.Button(Content.BakeButton))
{
NavMeshAssetManager.instance.StartBakingSurfaces(targets);
}
GUILayout.EndHorizontal();
// Inform when selected target is being baked
var bakeOperations = NavMeshAssetManager.instance.GetBakeOperations();
bool bakeInProgress = bakeOperations.Any(b =>
{
if (!targets.Contains(b.surface))
return false;
return b.bakeOperation != null;
});
if (bakeInProgress)
{
GUILayout.BeginVertical(EditorStyles.helpBox);
if (GUILayout.Button(Content.BakeInProgressButton, EditorStyles.linkLabel))
Progress.ShowDetails(false);
GUILayout.EndHorizontal();
}
}
}
[DrawGizmo(GizmoType.InSelectionHierarchy | GizmoType.Active | GizmoType.Pickable)]
static void RenderGizmoSelected(NavMeshSurface navSurface, GizmoType gizmoType)
{
navSurface.navMeshDataInstance.FlagAsInSelectionHierarchy();
DrawBoundingBoxGizmoAndIcon(navSurface, true);
}
[DrawGizmo(GizmoType.NotInSelectionHierarchy | GizmoType.Pickable)]
static void RenderGizmoNotSelected(NavMeshSurface navSurface, GizmoType gizmoType)
{
DrawBoundingBoxGizmoAndIcon(navSurface, false);
}
static void DrawBoundingBoxGizmoAndIcon(NavMeshSurface navSurface, bool selected)
{
var color = selected ? s_HandleColorSelected : s_HandleColor;
if (!navSurface.enabled)
color = s_HandleColorDisabled;
var oldColor = Gizmos.color;
var oldMatrix = Gizmos.matrix;
// Use the unscaled matrix for the NavMeshSurface
var localToWorld = Matrix4x4.TRS(navSurface.transform.position, navSurface.transform.rotation, Vector3.one);
Gizmos.matrix = localToWorld;
if (navSurface.collectObjects == CollectObjects.Volume)
{
Gizmos.color = color;
Gizmos.DrawWireCube(navSurface.center, navSurface.size);
if (selected && navSurface.enabled)
{
var colorTrans = new Color(color.r * 0.75f, color.g * 0.75f, color.b * 0.75f, color.a * 0.15f);
Gizmos.color = colorTrans;
Gizmos.DrawCube(navSurface.center, navSurface.size);
}
}
else
{
if (navSurface.navMeshData != null)
{
var bounds = navSurface.navMeshData.sourceBounds;
Gizmos.color = Color.grey;
Gizmos.DrawWireCube(bounds.center, bounds.size);
}
}
Gizmos.matrix = oldMatrix;
Gizmos.color = oldColor;
Gizmos.DrawIcon(navSurface.transform.position, "NavMeshSurface Icon", true);
}
void OnSceneGUI()
{
if (!editingCollider)
return;
var navSurface = (NavMeshSurface)target;
var color = navSurface.enabled ? s_HandleColor : s_HandleColorDisabled;
var localToWorld = Matrix4x4.TRS(navSurface.transform.position, navSurface.transform.rotation, Vector3.one);
using (new Handles.DrawingScope(color, localToWorld))
{
m_BoundsHandle.center = navSurface.center;
m_BoundsHandle.size = navSurface.size;
EditorGUI.BeginChangeCheck();
m_BoundsHandle.DrawHandle();
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(navSurface, Content.UndoModifyVolume);
Vector3 center = m_BoundsHandle.center;
Vector3 size = m_BoundsHandle.size;
navSurface.center = center;
navSurface.size = size;
EditorUtility.SetDirty(target);
}
}
}
[MenuItem("GameObject/AI/NavMesh Surface", false, 2000)]
public static void CreateNavMeshSurface(MenuCommand menuCommand)
{
var parent = menuCommand.context as GameObject;
NavMeshComponentsGUIUtility.CreateAndSelectGameObject("NavMesh Surface", parent, typeof(NavMeshSurface));
}
static class Content
{
public static readonly GUIContent AgentType = EditorGUIUtility.TrTextContent("Agent Type", "The NavMesh Agent type that uses the NavMesh Surface.");
public static readonly GUIContent DefaultArea = EditorGUIUtility.TrTextContent("Default Area", "The area type assumed for all the objects at the moment when Unity generates the NavMesh. Use the NavMesh Modifier component to override the area type of an object and its hierarchy.");
public static readonly GUIContent GenerateLinks = EditorGUIUtility.TrTextContent("Generate Links", "If enabled, collected objects will generate unidirectional links according to the drop height and jump distance values in the agent settings. Use the NavMesh Modifier component to override this behavior for an object and its hierarchy.");
public static readonly GUIContent UseGeometry = EditorGUIUtility.TrTextContent("Use Geometry", "The type of geometry to create the NavMesh from.");
public static readonly GUIContent ObjectCollectionHeader = EditorGUIUtility.TrTextContent("Object Collection", "Parameters that define how to select objects from the scene.");
public static readonly GUIContent CollectObjects = EditorGUIUtility.TrTextContent("Collect Objects", "Defines which GameObjects to use for baking.");
public static readonly GUIContent IncludeLayers = EditorGUIUtility.TrTextContent("Include Layers", "Define the layers on which GameObjects are included in the bake process.");
public static readonly GUIContent AdvancedHeader = EditorGUIUtility.TrTextContent("Advanced", "Parameters that control the level of detail and the structure of the navigation data during its creation.");
public static readonly GUIContent OverrideVoxelSize = EditorGUIUtility.TrTextContent("Override Voxel Size", "If enabled, uses the value below to control how accurately Unity processes the scene geometry when it creates the NavMesh.");
public static readonly GUIContent VoxelSize = EditorGUIUtility.TrTextContent("Voxel Size", "The width of cells in a grid used to sample the level geometry. The cell height is half of the width.");
public static readonly string VoxelSizeFormatString = L10n.Tr("{0:0.00} voxels per agent radius");
public static readonly string VoxelSizeHelpBox = L10n.Tr("Voxel size controls the accuracy with which Unity generates the NavMesh from the scene geometry. A good voxel size fits 2-4 voxels per agent radius. When you reduce the voxel size, both the accuracy and the bake duration increase.");
public static readonly GUIContent OverrideTileSize = EditorGUIUtility.TrTextContent("Override Tile Size", "If enabled, the value below overrides the size of the tiles that partition the NavMesh.");
public static readonly GUIContent TileSize = EditorGUIUtility.TrTextContent("Tile Size", "The number of voxels that determines the width of a square NavMesh tile. The created NavMesh is subdivided into a grid of tiles in order to make the bake process parallel and memory efficient. A value of 256 is a good balance between memory usage and NavMesh fragmentation.");
public static readonly string TileWorldSizeFormatString = L10n.Tr("{0:0.00} world units");
public static readonly string TileWorldSizeHelpBox = L10n.Tr("Tile size reduces the impact of scene changes to only a section of the NavMesh. Smaller tiles allow carving or rebuilding to produce localized changes but may generate more polygon data overall.");
public static readonly GUIContent MinimumRegionArea = EditorGUIUtility.TrTextContent("Minimum Region Area", "Allows you to cull away the small regions disconnected from the larger NavMesh.");
public static readonly GUIContent BuildHeightMesh = EditorGUIUtility.TrTextContent("Build Height Mesh", "Enables the creation of additional data used for determining more accurately the height at any position on the NavMesh.");
public static readonly GUIContent NavMeshData = EditorGUIUtility.TrTextContent("NavMesh Data", "Locate the asset file where the NavMesh is stored.");
public static readonly GUIContent ClearButton = EditorGUIUtility.TrTextContent("Clear", "Clear NavMesh data for this surface.");
public static readonly GUIContent BakeButton = EditorGUIUtility.TrTextContent("Bake", "Create the NavMesh with the current settings.");
public static readonly GUIContent BakeInProgressButton = EditorGUIUtility.TrTextContent("NavMesh baking is in progress.");
public static readonly string UndoModifyVolume = L10n.Tr("Modify NavMesh Surface Volume");
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1c32167dbf3314852b6006a288eb449b
timeCreated: 1476968447
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,349 @@
using UnityEditor;
using UnityEditor.AI;
using UnityEditor.Overlays;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.AI.Navigation.Editor
{
[Overlay(typeof(SceneView), k_OverlayId, "AI Navigation", defaultDisplay = false)]
[Icon(NavMeshComponentsGUIUtility.k_PackageEditorResourcesFolder + "Overlay/NavigationOverlay.png")]
internal class NavigationOverlay : Overlay
{
const string k_OverlayId = "AINavigationOverlay";
static class Style
{
internal static readonly GUIContent SurfacesSectionTexts =
EditorGUIUtility.TrTextContent("Surfaces");
internal static readonly GUIContent SurfacesSelectedOnlyTexts =
EditorGUIUtility.TrTextContent("Show Only Selected", "Check this to hide surfaces which are not part of the selection hierarchy");
internal static readonly GUIContent SurfacesNavMeshTexts =
EditorGUIUtility.TrTextContent("Show NavMesh", "Display navigation mesh using the associated area's color");
internal static readonly GUIContent SurfacesHeightMeshTexts =
EditorGUIUtility.TrTextContent("Show HeightMesh", "Display height mesh used for accurate vertical placement");
internal static readonly GUIContent AgentsSectionTexts =
EditorGUIUtility.TrTextContent("Agents");
internal static readonly GUIContent AgentsPathPolysTexts =
EditorGUIUtility.TrTextContent("Show Path Polygons", "Shows the polygons leading to goal.");
internal static readonly GUIContent AgentsPathNodesTexts =
EditorGUIUtility.TrTextContent("Show Path Query Nodes", "Shows the nodes expanded during last path query.");
internal static readonly GUIContent AgentsNeighboursTexts =
EditorGUIUtility.TrTextContent("Show Neighbours", "Show the agent neighbours considered during simulation.");
internal static readonly GUIContent AgentsWallsTexts =
EditorGUIUtility.TrTextContent("Show Walls", "Shows the wall segments handled during simulation.");
internal static readonly GUIContent AgentsAvoidanceTexts =
EditorGUIUtility.TrTextContent("Show Avoidance", "Shows the processed avoidance geometry from simulation.");
internal static readonly GUIContent AgentsAvoidancePendingDebugRequestTexts =
EditorGUIUtility.TrTextContent("Avoidance display is not valid until after next game update.", "Avoidance information will be computed on the next game update");
internal static readonly GUIContent AgentsAvoidanceDebugRequestsCountExceededTexts =
EditorGUIUtility.TrTextContent("", "Avoidance information display is limited to a fixed number of agents"); // This text is dynamic
internal static readonly GUIContent ObstaclesSectionTexts =
EditorGUIUtility.TrTextContent("Obstacles");
internal static readonly GUIContent ObstaclesCarveHullText =
EditorGUIUtility.TrTextContent("Show Carve Hull", "Shows the hull used to carve the obstacle from the NavMesh.");
internal static readonly GUIContent DeveloperModeSectionTexts =
EditorGUIUtility.TrTextContent("Developer Mode");
internal static readonly GUIContent SurfacesPortalsTexts =
EditorGUIUtility.TrTextContent("Show NavMesh Portals");
internal static readonly GUIContent SurfacesTileLinksTexts =
EditorGUIUtility.TrTextContent("Show NavMesh Tile Links");
internal static readonly GUIContent SurfacesHeightMeshBVTreeTexts =
EditorGUIUtility.TrTextContent("Show HeightMesh BV-Tree");
internal static readonly GUIContent SurfacesHeightMapsTexts =
EditorGUIUtility.TrTextContent("Show HeightMaps", "Display terrain height maps used for accurate vertical placement");
internal static readonly GUIContent SurfacesProximityGridTexts =
EditorGUIUtility.TrTextContent("Show Proximity Grid");
internal static readonly GUIContent NavigationVisualizationDisabledTexts =
EditorGUIUtility.TrTextContent("Navigation visualization is not available in prefab edition.", "");
}
VisualElement m_RootPanel;
VisualElement m_OptionsPanel;
VisualElement m_AgentFoldOut;
HelpBox m_VisualizationDisabledHelpBox;
HelpBox m_AgentCountWarning;
HelpBox m_AgentPendingRequestWarning;
public override void OnCreated()
{
base.OnCreated();
NavMeshEditorHelpers.agentRejectedDebugInfoRequestsCountChanged += OnAgentRejectedDebugInfoRequestsCountChanged;
NavMeshEditorHelpers.agentDebugRequestsPending += DisplayAgentPendingRequestWarningBox;
NavMeshEditorHelpers.agentDebugRequestsProcessed += HideAgentPendingRequestWarningBox;
PrefabStage.prefabStageOpened += OnPrefabStageOpened;
PrefabStage.prefabStageClosing += OnPrefabStageClosing;
}
public override void OnWillBeDestroyed()
{
NavMeshEditorHelpers.agentRejectedDebugInfoRequestsCountChanged -= OnAgentRejectedDebugInfoRequestsCountChanged;
NavMeshEditorHelpers.agentDebugRequestsPending -= DisplayAgentPendingRequestWarningBox;
NavMeshEditorHelpers.agentDebugRequestsProcessed -= HideAgentPendingRequestWarningBox;
PrefabStage.prefabStageOpened -= OnPrefabStageOpened;
PrefabStage.prefabStageClosing -= OnPrefabStageClosing;
base.OnWillBeDestroyed();
}
public override VisualElement CreatePanelContent()
{
m_RootPanel = new VisualElement();
m_OptionsPanel = new VisualElement();
m_RootPanel.Add(m_OptionsPanel);
m_VisualizationDisabledHelpBox = AddHelpBox(HelpBoxMessageType.Info, Style.NavigationVisualizationDisabledTexts, 200, 10, false);
var visualizationEnabled = PrefabStageUtility.GetCurrentPrefabStage() == null;
SetVisualizationEnabled(visualizationEnabled);
// Surfaces
var surfacesFoldout = AddFoldout(m_OptionsPanel, Style.SurfacesSectionTexts, 5);
AddToggle(surfacesFoldout, Style.SurfacesSelectedOnlyTexts, NavMeshVisualizationSettings.showOnlySelectedSurfaces,
(evt => NavMeshVisualizationSettings.showOnlySelectedSurfaces = evt.newValue));
AddToggle(surfacesFoldout, Style.SurfacesNavMeshTexts, NavMeshVisualizationSettings.showNavMesh,
evt => NavMeshVisualizationSettings.showNavMesh = evt.newValue);
AddToggle(surfacesFoldout, Style.SurfacesHeightMeshTexts, NavMeshVisualizationSettings.showHeightMesh,
evt => NavMeshVisualizationSettings.showHeightMesh = evt.newValue);
// Agents
m_AgentFoldOut = AddFoldout(m_OptionsPanel, Style.AgentsSectionTexts, 5);
AddToggle(m_AgentFoldOut, Style.AgentsPathPolysTexts, NavMeshVisualizationSettings.showAgentPath, evt => NavMeshVisualizationSettings.showAgentPath = evt.newValue);
AddToggle(m_AgentFoldOut, Style.AgentsPathNodesTexts, NavMeshVisualizationSettings.showAgentPathInfo,
evt => NavMeshVisualizationSettings.showAgentPathInfo = evt.newValue);
AddToggle(m_AgentFoldOut, Style.AgentsNeighboursTexts, NavMeshVisualizationSettings.showAgentNeighbours,
evt => NavMeshVisualizationSettings.showAgentNeighbours = evt.newValue);
AddToggle(m_AgentFoldOut, Style.AgentsWallsTexts, NavMeshVisualizationSettings.showAgentWalls, evt => NavMeshVisualizationSettings.showAgentWalls = evt.newValue);
AddToggle(m_AgentFoldOut, Style.AgentsAvoidanceTexts, NavMeshVisualizationSettings.showAgentAvoidance,
evt => NavMeshVisualizationSettings.showAgentAvoidance = evt.newValue);
// Create avoidance requests count warning box and display it if needed
m_AgentCountWarning = AddHelpBox(HelpBoxMessageType.Warning, Style.AgentsAvoidanceDebugRequestsCountExceededTexts, 180, 5, false);
NavMeshEditorHelpers.GetAgentsDebugInfoRejectedRequestsCount(out var rejected, out var allowed);
if (rejected > 0)
{
DisplayAgentCountWarningBox(rejected, allowed);
}
// Create avoidance pending requests warning box and display it if needed
m_AgentPendingRequestWarning = AddHelpBox(HelpBoxMessageType.Warning, Style.AgentsAvoidancePendingDebugRequestTexts, 180, 5, false);
if (NavMeshEditorHelpers.HasPendingAgentDebugInfoRequests())
{
DisplayAgentPendingRequestWarningBox();
}
// Obstacles
var obstaclesFoldout = AddFoldout(m_OptionsPanel, Style.ObstaclesSectionTexts, 5);
AddToggle(obstaclesFoldout, Style.ObstaclesCarveHullText, NavMeshVisualizationSettings.showObstacleCarveHull,
evt => NavMeshVisualizationSettings.showObstacleCarveHull = evt.newValue);
// Developer Mode only
if (Unsupported.IsDeveloperMode())
{
var developerModeFoldout = AddFoldout(m_OptionsPanel, Style.DeveloperModeSectionTexts, 5);
AddToggle(developerModeFoldout, Style.SurfacesPortalsTexts, NavMeshVisualizationSettings.showNavMeshPortals,
evt => NavMeshVisualizationSettings.showNavMeshPortals = evt.newValue);
AddToggle(developerModeFoldout, Style.SurfacesTileLinksTexts, NavMeshVisualizationSettings.showNavMeshLinks,
evt => NavMeshVisualizationSettings.showNavMeshLinks = evt.newValue);
AddToggle(developerModeFoldout, Style.SurfacesHeightMeshBVTreeTexts, NavMeshVisualizationSettings.showHeightMeshBVTree,
evt => NavMeshVisualizationSettings.showHeightMeshBVTree = evt.newValue);
AddToggle(developerModeFoldout, Style.SurfacesHeightMapsTexts, NavMeshVisualizationSettings.showHeightMaps,
evt => NavMeshVisualizationSettings.showHeightMaps = evt.newValue);
AddToggle(developerModeFoldout, Style.SurfacesProximityGridTexts, NavMeshVisualizationSettings.showProximityGrid,
evt => NavMeshVisualizationSettings.showProximityGrid = evt.newValue);
}
return m_RootPanel;
}
void OnAgentRejectedDebugInfoRequestsCountChanged(int rejected, int allowed)
{
if (rejected == 0)
{
HideAgentCountWarningBox();
}
else
{
DisplayAgentCountWarningBox(rejected, allowed);
}
}
void OnPrefabStageOpened(PrefabStage obj)
{
SetVisualizationEnabled(false);
}
void OnPrefabStageClosing(PrefabStage obj)
{
SetVisualizationEnabled(true);
}
static Foldout AddFoldout(VisualElement parent, GUIContent text, int padding)
{
var prefName = $"AINavigationOverlay_Foldout_{text.text}";
var foldout = new Foldout
{
text = text.text,
tooltip = text.tooltip,
style =
{
paddingBottom = padding
},
value = EditorPrefs.GetBool(prefName, true)
};
foldout.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(prefName, evt.newValue);
#if OVERLAY_REFRESH_API
var sceneView = EditorWindow.GetWindow<SceneView>();
if (sceneView.TryGetOverlay(k_OverlayId, out var overlay))
overlay.RefreshPopup();
#endif
});
parent.Add(foldout);
return foldout;
}
static void AddToggle(VisualElement parent, GUIContent text, bool parameter, EventCallback<ChangeEvent<bool>> callback)
{
// Create toggle element with the desired text content
var toggle = new Toggle
{
label = text.text,
value = parameter,
tooltip = text.tooltip,
style =
{
marginBottom = 0 // To compact a bit the checkboxes list layout
}
};
// Add padding to guarantee a minimum separation between labels and checkboxes
toggle.labelElement.style.paddingRight = 20;
// Look for the checkbox container and make it align the checkbox to the right (so that all checkboxes are justified)
foreach (VisualElement child in toggle.Children())
{
if (child != toggle.labelElement)
{
child.style.justifyContent = Justify.FlexEnd;
break;
}
}
toggle.RegisterCallback(callback);
parent.Add(toggle);
}
static HelpBox AddHelpBox(HelpBoxMessageType messageType, GUIContent text, int maxWidth, int verticalMargin, bool visible)
{
var helpBox = new HelpBox(text.text, messageType)
{
tooltip = text.tooltip,
style =
{
marginBottom = verticalMargin,
marginTop = verticalMargin,
maxWidth = maxWidth,
alignSelf = Align.Center,
},
visible = visible
};
return helpBox;
}
void DisplayAgentCountWarningBox(int rejected, int allowed)
{
m_AgentCountWarning.text = $"Avoidance visualization can be drawn for {allowed} agents ({allowed + rejected} selected).";
if (!m_AgentCountWarning.visible)
{
m_AgentCountWarning.visible = true;
m_AgentFoldOut.Add(m_AgentCountWarning);
}
}
void HideAgentCountWarningBox()
{
if (m_AgentCountWarning.visible)
{
m_AgentCountWarning.visible = false;
m_AgentFoldOut.Remove(m_AgentCountWarning);
}
}
void DisplayAgentPendingRequestWarningBox()
{
if (!m_AgentPendingRequestWarning.visible)
{
m_AgentPendingRequestWarning.visible = true;
m_AgentFoldOut.Add(m_AgentPendingRequestWarning);
}
}
void HideAgentPendingRequestWarningBox()
{
if (m_AgentPendingRequestWarning.visible)
{
m_AgentPendingRequestWarning.visible = false;
m_AgentFoldOut.Remove(m_AgentPendingRequestWarning);
}
}
void SetVisualizationEnabled(bool enabled)
{
if (m_VisualizationDisabledHelpBox == null)
return;
if (enabled)
{
if (m_VisualizationDisabledHelpBox.visible)
{
m_RootPanel.Remove(m_VisualizationDisabledHelpBox);
m_OptionsPanel.SetEnabled(true);
m_OptionsPanel.tooltip = "";
m_VisualizationDisabledHelpBox.visible = false;
}
}
else
{
if (!m_VisualizationDisabledHelpBox.visible)
{
m_RootPanel.Insert(0, m_VisualizationDisabledHelpBox);
m_OptionsPanel.SetEnabled(false);
m_OptionsPanel.tooltip = Style.NavigationVisualizationDisabledTexts.tooltip;
m_VisualizationDisabledHelpBox.visible = true;
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c660403886dc4a449f55327665b10bbb
timeCreated: 1629385811

View File

@@ -0,0 +1,74 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using VisualizationSettings = UnityEditor.AI.NavMeshVisualizationSettings;
namespace Unity.AI.Navigation.Editor
{
class NavigationPreferencesProvider : SettingsProvider
{
class Styles
{
internal static readonly GUIContent NavMeshVisualizationSettingsLabel =
EditorGUIUtility.TrTextContent("NavMesh Visualization Settings");
internal static readonly GUIContent SelectedSurfacesOpacityLabel =
EditorGUIUtility.TrTextContent("Selected Surfaces Opacity", "Controls the mesh transparency for surfaces inside the selection hierarchy");
internal static readonly GUIContent UnselectedSurfacesOpacityLabel =
EditorGUIUtility.TrTextContent("Unselected Surfaces Opacity", "Controls the mesh transparency for surfaces outside the selection hierarchy");
internal static readonly GUIContent HeightMeshColorLabel =
EditorGUIUtility.TrTextContent("Height Mesh Color", "Color used to display height mesh information in the scene view");
internal static readonly GUIContent ResetVisualizationSettingsButtonLabel =
EditorGUIUtility.TrTextContent("Reset to Defaults", "Revert visualization settings to their original values. Customized values will be lost");
}
NavigationPreferencesProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null)
: base(path, scopes, keywords)
{
}
public override void OnGUI(string searchContext)
{
using (new SettingsWindow.GUIScope())
{
EditorGUILayout.LabelField(Styles.NavMeshVisualizationSettingsLabel, EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
// Visualization settings
VisualizationSettings.selectedSurfacesOpacity = EditorGUILayout.Slider(Styles.SelectedSurfacesOpacityLabel, VisualizationSettings.selectedSurfacesOpacity, 0, 1);
VisualizationSettings.unselectedSurfacesOpacity = EditorGUILayout.Slider(Styles.UnselectedSurfacesOpacityLabel, VisualizationSettings.unselectedSurfacesOpacity, 0, 1);
VisualizationSettings.heightMeshColor = EditorGUILayout.ColorField(Styles.HeightMeshColorLabel, VisualizationSettings.heightMeshColor);
EditorGUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
// Option to reset the visualization settings to their default values
if (GUILayout.Button(Styles.ResetVisualizationSettingsButtonLabel, GUILayout.Width(160)))
{
VisualizationSettings.ResetSelectedSurfacesOpacity();
VisualizationSettings.ResetUnselectedSurfacesOpacity();
VisualizationSettings.ResetHeightMeshColor();
}
EditorGUILayout.Space(10);
EditorGUILayout.EndHorizontal();
// Repaint the scene view when settings changed to update the visualization accordingly
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
}
}
[SettingsProvider]
internal static SettingsProvider CreateNavigationProjectSettingProvider()
{
var provider = new NavigationPreferencesProvider("Preferences/AI Navigation", SettingsScope.User, GetSearchKeywordsFromGUIContentProperties<Styles>());
return provider;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8c8febe35b734f4eb15f23d12e5faed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,414 @@
using UnityEngine;
using UnityEditor.AI;
using UnityEditorInternal;
using Object = UnityEngine.Object;
using UnityEditor;
namespace Unity.AI.Navigation.Editor
{
[InitializeOnLoad]
[EditorWindowTitle(title = "Navigation")]
internal class NavigationWindow : EditorWindow
{
static NavigationWindow s_NavigationWindow;
// Scene based bake configuration
SerializedObject m_SettingsObject;
// Project based configuration
SerializedObject m_NavMeshProjectSettingsObject;
SerializedProperty m_Areas;
SerializedProperty m_AgentTypes;
SerializedProperty m_SettingNames;
Vector2 m_ScrollPos = Vector2.zero;
bool m_Advanced;
ReorderableList m_AreasList;
ReorderableList m_AgentTypeList;
enum Mode
{
AgentTypeSettings = 0,
AreaSettings = 1,
}
Mode m_Mode = Mode.AgentTypeSettings;
static class Styles
{
internal static readonly GUIContent k_AgentTypesHeader = EditorGUIUtility.TrTextContent("Agent Types");
internal static readonly GUIContent k_NameLabel = EditorGUIUtility.TrTextContent("Name");
internal static readonly GUIContent k_CostLabel = EditorGUIUtility.TrTextContent("Cost");
internal static readonly GUIContent[] k_ModeToggles =
{
EditorGUIUtility.TrTextContent("Agents", "Navmesh agent settings."),
EditorGUIUtility.TrTextContent("Areas", "Navmesh area settings."),
};
internal static readonly GUIStyle k_ContentMargins = new GUIStyle
{
padding = new RectOffset(4, 4, 4, 4)
};
};
static NavigationWindow()
{
NavMeshEditorHelpers.areaSettingsClicked += OpenAreaSettings;
NavMeshEditorHelpers.agentTypeSettingsClicked += OpenAgentSettings;
}
[MenuItem("Window/AI/Navigation", false, 1)]
public static void SetupWindow()
{
var window = GetWindow<NavigationWindow>();
window.minSize = new Vector2(300, 360);
}
static void OpenAreaSettings()
{
SetupWindow();
if (s_NavigationWindow == null)
return;
s_NavigationWindow.m_Mode = Mode.AreaSettings;
s_NavigationWindow.InitProjectSettings();
s_NavigationWindow.InitAgentTypes();
}
static void OpenAgentSettings(int agentTypeID)
{
SetupWindow();
if (s_NavigationWindow == null)
return;
s_NavigationWindow.m_Mode = Mode.AgentTypeSettings;
s_NavigationWindow.InitProjectSettings();
s_NavigationWindow.InitAgentTypes();
s_NavigationWindow.m_AgentTypeList.index = -1;
for (int i = 0; i < s_NavigationWindow.m_AgentTypes.arraySize; i++)
{
SerializedProperty agentType = s_NavigationWindow.m_AgentTypes.GetArrayElementAtIndex(i);
SerializedProperty idProp = agentType.FindPropertyRelative("agentTypeID");
if (idProp.intValue == agentTypeID)
{
s_NavigationWindow.m_AgentTypeList.index = i;
break;
}
}
}
public void OnEnable()
{
var iconPath = $"{NavMeshComponentsGUIUtility.k_PackageEditorResourcesFolder}NavigationWindowIcon.asset";
titleContent = EditorGUIUtility.TrTextContentWithIcon("Navigation", iconPath);
s_NavigationWindow = this;
EditorApplication.searchChanged += Repaint;
Repaint();
}
void InitProjectSettings()
{
if (m_NavMeshProjectSettingsObject == null)
{
Object obj = Unsupported.GetSerializedAssetInterfaceSingleton("NavMeshProjectSettings");
m_NavMeshProjectSettingsObject = new SerializedObject(obj);
}
}
void InitAreas()
{
if (m_Areas == null)
{
m_Areas = m_NavMeshProjectSettingsObject.FindProperty("areas");
}
if (m_AreasList == null)
{
m_AreasList = new ReorderableList(m_NavMeshProjectSettingsObject, m_Areas, false, true, false, false);
m_AreasList.drawElementCallback = DrawAreaListElement;
m_AreasList.drawHeaderCallback = DrawAreaListHeader;
m_AreasList.elementHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
}
}
void InitAgentTypes()
{
if (m_AgentTypes == null)
{
m_AgentTypes = m_NavMeshProjectSettingsObject.FindProperty("m_Settings");
m_SettingNames = m_NavMeshProjectSettingsObject.FindProperty("m_SettingNames");
}
if (m_AgentTypeList == null)
{
m_AgentTypeList = new ReorderableList(m_NavMeshProjectSettingsObject, m_AgentTypes, false, true, true, true);
m_AgentTypeList.drawElementCallback = DrawAgentTypeListElement;
m_AgentTypeList.drawHeaderCallback = DrawAgentTypeListHeader;
m_AgentTypeList.onAddCallback = AddAgentType;
m_AgentTypeList.onRemoveCallback = RemoveAgentType;
m_AgentTypeList.elementHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
}
}
// This code is replicated from the NavMesh debug draw code.
int Bit(int a, int b)
{
return (a & (1 << b)) >> b;
}
Color GetAreaColor(int i)
{
if (i == 0)
return new Color(0, 0.75f, 1.0f, 0.5f);
int r = (Bit(i, 4) + Bit(i, 1) * 2 + 1) * 63;
int g = (Bit(i, 3) + Bit(i, 2) * 2 + 1) * 63;
int b = (Bit(i, 5) + Bit(i, 0) * 2 + 1) * 63;
return new Color(r / 255.0f, g / 255.0f, b / 255.0f, 0.5f);
}
public void OnDisable()
{
s_NavigationWindow = null;
EditorApplication.searchChanged -= Repaint;
}
void OnSelectionChange()
{
m_ScrollPos = Vector2.zero;
}
void ModeToggle()
{
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
m_Mode = (Mode)GUILayout.Toolbar((int)m_Mode, Styles.k_ModeToggles, "LargeButton", GUI.ToolbarButtonSize.FitToContents);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
static void GetAreaListRects(Rect rect, out Rect stripeRect, out Rect labelRect, out Rect nameRect, out Rect costRect)
{
float stripeWidth = EditorGUIUtility.singleLineHeight * 0.8f;
float labelWidth = EditorGUIUtility.singleLineHeight * 5;
float costWidth = EditorGUIUtility.singleLineHeight * 4;
float nameWidth = rect.width - stripeWidth - labelWidth - costWidth;
float x = rect.x;
stripeRect = new Rect(x, rect.y, stripeWidth - 4, rect.height);
x += stripeWidth;
labelRect = new Rect(x, rect.y, labelWidth - 4, rect.height);
x += labelWidth;
nameRect = new Rect(x, rect.y, nameWidth - 4, rect.height);
x += nameWidth;
costRect = new Rect(x, rect.y, costWidth, rect.height);
}
void DrawAreaListHeader(Rect rect)
{
GetAreaListRects(rect, out _, out _, out Rect nameRect, out Rect costRect);
GUI.Label(nameRect, Styles.k_NameLabel);
GUI.Label(costRect, Styles.k_CostLabel);
}
void DrawAreaListElement(Rect rect, int index, bool selected, bool focused)
{
SerializedProperty areaProp = m_Areas.GetArrayElementAtIndex(index);
if (areaProp == null)
return;
SerializedProperty nameProp = areaProp.FindPropertyRelative("name");
SerializedProperty costProp = areaProp.FindPropertyRelative("cost");
if (nameProp == null || costProp == null)
return;
rect.height -= 2; // nicer looking with selected list row and a text field in it
bool builtInLayer;
bool allowChangeName;
bool allowChangeCost;
switch (index)
{
case 0: // Default
builtInLayer = true;
allowChangeName = false;
allowChangeCost = true;
break;
case 1: // NonWalkable
builtInLayer = true;
allowChangeName = false;
allowChangeCost = false;
break;
case 2: // Jump
builtInLayer = true;
allowChangeName = false;
allowChangeCost = true;
break;
default:
builtInLayer = false;
allowChangeName = true;
allowChangeCost = true;
break;
}
GetAreaListRects(rect, out Rect stripeRect, out Rect labelRect, out Rect nameRect, out Rect costRect);
bool oldEnabled = GUI.enabled;
Color color = GetAreaColor(index);
Color dimmed = new Color(color.r * 0.1f, color.g * 0.1f, color.b * 0.1f, 0.6f);
EditorGUI.DrawRect(stripeRect, color);
EditorGUI.DrawRect(new Rect(stripeRect.x, stripeRect.y, 1, stripeRect.height), dimmed);
EditorGUI.DrawRect(new Rect(stripeRect.x + stripeRect.width - 1, stripeRect.y, 1, stripeRect.height), dimmed);
EditorGUI.DrawRect(new Rect(stripeRect.x + 1, stripeRect.y, stripeRect.width - 2, 1), dimmed);
EditorGUI.DrawRect(new Rect(stripeRect.x + 1, stripeRect.y + stripeRect.height - 1, stripeRect.width - 2, 1), dimmed);
if (builtInLayer)
GUI.Label(labelRect, EditorGUIUtility.TrTempContent("Built-in " + index));
else
GUI.Label(labelRect, EditorGUIUtility.TrTempContent("User " + index));
int oldIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
GUI.enabled = oldEnabled && allowChangeName;
EditorGUI.PropertyField(nameRect, nameProp, GUIContent.none);
GUI.enabled = oldEnabled && allowChangeCost;
EditorGUI.PropertyField(costRect, costProp, GUIContent.none);
GUI.enabled = oldEnabled;
EditorGUI.indentLevel = oldIndent;
}
static void AddAgentType(ReorderableList list)
{
UnityEngine.AI.NavMesh.CreateSettings();
list.index = UnityEngine.AI.NavMesh.GetSettingsCount() - 1;
}
void RemoveAgentType(ReorderableList list)
{
SerializedProperty agentTypeProp = m_AgentTypes.GetArrayElementAtIndex(list.index);
if (agentTypeProp == null)
return;
SerializedProperty idProp = agentTypeProp.FindPropertyRelative("agentTypeID");
if (idProp == null)
return;
// Cannot delete default.
if (idProp.intValue == 0)
return;
m_SettingNames.DeleteArrayElementAtIndex(list.index);
ReorderableList.defaultBehaviours.DoRemoveButton(list);
}
static void DrawAgentTypeListHeader(Rect rect)
{
GUI.Label(rect, Styles.k_AgentTypesHeader);
}
void DrawAgentTypeListElement(Rect rect, int index, bool selected, bool focused)
{
SerializedProperty agentProp = m_AgentTypes.GetArrayElementAtIndex(index);
if (agentProp == null)
return;
SerializedProperty idProp = agentProp.FindPropertyRelative("agentTypeID");
if (idProp == null)
return;
rect.height -= 2; // nicer looking with selected list row and a text field in it
bool isDefault = idProp.intValue == 0;
using (new EditorGUI.DisabledScope(isDefault))
{
var settingsName = UnityEngine.AI.NavMesh.GetSettingsNameFromID(idProp.intValue);
GUI.Label(rect, EditorGUIUtility.TrTempContent(settingsName));
}
}
public void OnGUI()
{
EditorGUILayout.Space();
ModeToggle();
EditorGUILayout.Space();
InitProjectSettings();
m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos);
switch (m_Mode)
{
case Mode.AreaSettings:
AreaSettings();
break;
case Mode.AgentTypeSettings:
AgentTypeSettings();
break;
}
EditorGUILayout.EndScrollView();
}
void AreaSettings()
{
if (m_Areas == null)
InitAreas();
m_NavMeshProjectSettingsObject.Update();
using (new GUILayout.VerticalScope(Styles.k_ContentMargins))
{
m_AreasList.DoLayoutList();
}
m_NavMeshProjectSettingsObject.ApplyModifiedProperties();
}
void AgentTypeSettings()
{
if (m_AgentTypes == null)
InitAgentTypes();
m_NavMeshProjectSettingsObject.Update();
if (m_AgentTypeList.index < 0)
m_AgentTypeList.index = 0;
using (new GUILayout.VerticalScope(Styles.k_ContentMargins))
{
m_AgentTypeList.DoLayoutList();
}
if (m_AgentTypeList.index >= 0 && m_AgentTypeList.index < m_AgentTypes.arraySize)
{
SerializedProperty nameProp = m_SettingNames.GetArrayElementAtIndex(m_AgentTypeList.index);
SerializedProperty selectedAgentType = m_AgentTypes.GetArrayElementAtIndex(m_AgentTypeList.index);
SerializedProperty radiusProp = selectedAgentType.FindPropertyRelative("agentRadius");
SerializedProperty heightProp = selectedAgentType.FindPropertyRelative("agentHeight");
SerializedProperty stepHeightProp = selectedAgentType.FindPropertyRelative("agentClimb");
SerializedProperty maxSlopeProp = selectedAgentType.FindPropertyRelative("agentSlope");
SerializedProperty ledgeDropHeightProp = selectedAgentType.FindPropertyRelative("ledgeDropHeight");
SerializedProperty jumpDistanceProp = selectedAgentType.FindPropertyRelative("maxJumpAcrossDistance");
const float kDiagramHeight = 120.0f;
Rect agentDiagramRect = EditorGUILayout.GetControlRect(false, kDiagramHeight);
NavMeshEditorHelpers.DrawAgentDiagram(agentDiagramRect, radiusProp.floatValue, heightProp.floatValue, stepHeightProp.floatValue, maxSlopeProp.floatValue);
EditorGUILayout.PropertyField(nameProp, EditorGUIUtility.TrTempContent("Name"));
EditorGUILayout.PropertyField(radiusProp, EditorGUIUtility.TrTempContent("Radius"));
EditorGUILayout.PropertyField(heightProp, EditorGUIUtility.TrTempContent("Height"));
EditorGUILayout.PropertyField(stepHeightProp, EditorGUIUtility.TrTempContent("Step Height"));
const float kMaxSlopeAngle = 60.0f;
EditorGUILayout.Slider(maxSlopeProp, 0.0f, kMaxSlopeAngle, EditorGUIUtility.TrTextContent("Max Slope"));
EditorGUILayout.Space();
EditorGUILayout.LabelField(EditorGUIUtility.TrTempContent("Generated Links"), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(ledgeDropHeightProp, EditorGUIUtility.TrTempContent("Drop Height"));
EditorGUILayout.PropertyField(jumpDistanceProp, EditorGUIUtility.TrTempContent("Jump Distance"));
}
EditorGUILayout.Space();
m_NavMeshProjectSettingsObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e86a4b8c5b2b8425b9a3a007619c3a88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
{
"name": "Unity.AI.Navigation.Editor",
"rootNamespace": "",
"references": [
"GUID:8c4dd21966739024fbd72155091d199e",
"GUID:c57630adab7a340ec94f10e4fcac12f1"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "Unity",
"expression": "6000.0.22",
"define": "OVERLAY_REFRESH_API"
}
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 86c9d8e67265f41469be06142c397d17
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aaa4efef82a9346dba667d74ff3d5075
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.AI.Navigation.Editor.Tests")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebc7acf85aca94da2a3cc5c2f5236c45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using Unity.AI.Navigation.Editor.Converter;
using UnityEditor;
namespace Unity.AI.Navigation.Updater
{
internal class NavMeshSceneConverter : SystemConverter
{
public override string name => "NavMesh Scene Converter";
public override string info => "Reassigns the legacy baked NavMesh to a NavMeshSurface on a game object named 'Navigation'.\nAdds a NavMeshModifier component to each game object marked as Navigation Static.";
public override Type container => typeof(NavigationConverterContainer);
List<string> m_AssetsToConvert = new List<string>();
public override void OnInitialize(InitializeConverterContext context, Action callback)
{
string[] allAssetPaths = AssetDatabase.GetAllAssetPaths();
foreach (string path in allAssetPaths)
{
if (NavMeshUpdaterUtility.IsSceneReferencingLegacyNavMesh(path))
{
ConverterItemDescriptor desc = new ConverterItemDescriptor()
{
name = Path.GetFileNameWithoutExtension(path),
info = path,
warningMessage = String.Empty,
};
m_AssetsToConvert.Add(path);
context.AddAssetToConvert(desc);
}
}
callback.Invoke();
}
public override void OnRun(ref RunItemContext context)
{
var items = context.items;
for (var i = 0; i < items.Length; ++i)
{
var success = NavMeshUpdaterUtility.ConvertScene(items[i].descriptor.info);
if (!success)
{
var index = items[i].index;
context.didFail[index] = true;
context.info[index] = "Failed to convert scene.";
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5747e6c564aaa4e468d457a54f2e508d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,165 @@
using System;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
namespace Unity.AI.Navigation.Updater
{
internal static class NavMeshUpdaterUtility
{
const string k_NavMeshSettingsPropertyPath = "NavMeshSettings";
const string k_NavMeshDataPropertyPath = "m_NavMeshData";
const string k_EmptyFileIdSerialization = "{fileID: 0}";
const string k_DefaultNavMeshSurfaceName = "Navigation";
const int k_WalkableAreaId = 0;
public static bool ConvertScene(string path)
{
Scene previousActiveScene = SceneManager.GetActiveScene();
OpenAndSetActiveScene(path, out Scene convertedScene, out bool alreadyOpened);
// Retrieve the legacy NavMesh data from the active scene
#pragma warning disable CS0618 // UnityEditor.AI.NavMeshBuilder is necessary in this implementation
var settingObject = new SerializedObject(UnityEditor.AI.NavMeshBuilder.navMeshSettingsObject);
#pragma warning restore CS0618
var navMeshDataProperty = settingObject.FindProperty(k_NavMeshDataPropertyPath);
var navMeshData = navMeshDataProperty.objectReferenceValue as NavMeshData;
if (navMeshData == null)
{
Debug.LogWarning("The NavMesh asset referenced in the scene is missing or corrupted.");
return false;
}
// Convert static Navigation Flags with a NavMeshModifier
GameObject[] rootObjects = convertedScene.GetRootGameObjects();
var sceneObjects = SceneModeUtility.GetObjects(rootObjects, true);
foreach (GameObject go in sceneObjects)
ConvertStaticValues(go);
// Create a global NavMeshSurface and copy legacy NavMesh settings
var surfaceObject = new GameObject(k_DefaultNavMeshSurfaceName, typeof(NavMeshSurface));
var navMeshSurface = surfaceObject.GetComponent<NavMeshSurface>();
navMeshSurface.navMeshData = navMeshData;
NavMeshBuildSettings settings = navMeshData.buildSettings;
navMeshSurface.collectObjects = CollectObjects.MarkedWithModifier;
navMeshSurface.useGeometry = NavMeshCollectGeometry.RenderMeshes;
navMeshSurface.overrideVoxelSize = settings.overrideVoxelSize;
navMeshSurface.voxelSize = settings.voxelSize;
navMeshSurface.overrideTileSize = settings.overrideTileSize;
navMeshSurface.tileSize = settings.tileSize;
navMeshSurface.minRegionArea = settings.minRegionArea;
navMeshSurface.buildHeightMesh = settings.buildHeightMesh;
// Remove NavMeshData reference from the scene
navMeshDataProperty.objectReferenceValue = null;
settingObject.ApplyModifiedProperties();
// Rename NavMesh asset
var assetPath = AssetDatabase.GetAssetPath(navMeshData);
AssetDatabase.RenameAsset(assetPath, "NavMesh-" + navMeshSurface.name + ".asset");
AssetDatabase.Refresh();
EditorSceneManager.SaveScene(convertedScene);
navMeshSurface.AddData();
if (!alreadyOpened)
EditorSceneManager.CloseScene(convertedScene, true);
SceneManager.SetActiveScene(previousActiveScene);
return true;
}
private static void OpenAndSetActiveScene(string path, out Scene scene, out bool alreadyOpened)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded)
continue;
if (path == scene.path)
{
if (SceneManager.GetActiveScene() != scene)
SceneManager.SetActiveScene(scene);
alreadyOpened = true;
return;
}
}
scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
SceneManager.SetActiveScene(scene);
alreadyOpened = false;
}
private static void ConvertStaticValues(GameObject go)
{
// Disable CS0618 warning about StaticEditorFlags.NavigationStatic and StaticEditorFlags.OffMeshLinkGeneration being deprecated
#pragma warning disable 618
var staticFlags = GameObjectUtility.GetStaticEditorFlags(go);
if ((staticFlags & StaticEditorFlags.NavigationStatic) != 0)
{
NavMeshModifier modifier = go.AddComponent<NavMeshModifier>();
modifier.area = GameObjectUtility.GetNavMeshArea(go);
if (modifier.area != k_WalkableAreaId)
modifier.overrideArea = true;
if ((staticFlags & StaticEditorFlags.OffMeshLinkGeneration) != 0)
{
modifier.overrideGenerateLinks = true;
modifier.generateLinks = true;
}
staticFlags &= ~StaticEditorFlags.NavigationStatic;
GameObjectUtility.SetStaticEditorFlags(go, staticFlags);
}
#pragma warning restore 618
}
internal static bool IsSceneReferencingLegacyNavMesh(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (path.StartsWith("Packages"))
return false;
if (!path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
return false;
using (StreamReader file = File.OpenText(path))
{
string line;
bool skipUntilSettings = true;
while ((line = file.ReadLine()) != null)
{
if (skipUntilSettings)
{
if (line.Contains($"{k_NavMeshSettingsPropertyPath}:"))
skipUntilSettings = false;
}
else
{
if (line.Contains($"{k_NavMeshDataPropertyPath}:"))
{
return !line.Contains(k_EmptyFileIdSerialization);
}
}
}
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7df3797ad5e64406fb77aae77b5d42dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using Unity.AI.Navigation.Editor.Converter;
namespace Unity.AI.Navigation.Updater
{
internal sealed class NavigationConverterContainer : SystemConverterContainer
{
public override string name => "Navigation Updater";
public override string info => "* Converts scenes baked with the built-in NavMesh to the component-based version.\n" +
"* Replaces Navigation Static flags with NavMeshModifier components.\n" +
"* Turns OffMesh Link components into NavMesh Link components and preserves their properties.";
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14f573b87e1ac42568de3d97da3a0f03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using Unity.AI.Navigation.Editor.Converter;
using UnityEditor;
using UnityEngine;
namespace Unity.AI.Navigation.Updater
{
internal static class NavigationUpdaterEditor
{
[MenuItem("Window/AI/Navigation Updater", false, 50)]
public static void ShowWindow()
{
var wnd = EditorWindow.GetWindow<SystemConvertersEditor>();
wnd.titleContent = new GUIContent("Navigation Updater");
wnd.DontSaveToLayout(wnd);
wnd.maxSize = new Vector2(650f, 4000f);
wnd.minSize = new Vector2(650f, 400f);
wnd.Show();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 582e6909cfc304d93b58ee91d453d156
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using Unity.AI.Navigation.Editor.Converter;
using UnityEditor;
namespace Unity.AI.Navigation.Updater
{
internal sealed class OffMeshLinkConverter : SystemConverter
{
public override string name => "OffMesh Link Converter";
public override string info => "Creates NavMesh Link components that match and replace existing OffMesh Link components. \n" +
"Ensure the selected scene or prefab files are writable prior to running the Converter.";
public override Type container => typeof(NavigationConverterContainer);
public override void OnInitialize(InitializeConverterContext context, Action callback)
{
var objectsToConvert = OffMeshLinkUpdaterUtility.FindObjectsToConvert();
foreach (var guid in objectsToConvert)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var desc = new ConverterItemDescriptor()
{
name = Path.GetFileNameWithoutExtension(path),
info = path,
additionalData = guid
};
context.AddAssetToConvert(desc);
}
callback.Invoke();
}
public override void OnRun(ref RunItemContext context)
{
var convertList = new List<string>(context.items.Length);
for (var i = 0; i < context.items.Length; ++i)
{
var guid = context.items[i].descriptor.additionalData;
convertList.Add(guid);
}
OffMeshLinkUpdaterUtility.Convert(convertList, out var failedConversions);
foreach (var conversionItem in failedConversions)
{
var index = conversionItem.itemIndex;
context.didFail[index] = true;
context.info[index] = conversionItem.failureMessage;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 202accd362464ac0b604bb6d336eb8aa
timeCreated: 1710163840

View File

@@ -0,0 +1,523 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
namespace Unity.AI.Navigation.Updater
{
#pragma warning disable 618
internal static class OffMeshLinkUpdaterUtility
{
/// <summary>
/// A structure holding the information of failed conversions.
/// </summary>
public struct FailedConversion
{
public int itemIndex;
public string failureMessage;
}
/// <summary>
/// Find all prefabs and scenes that contain OffMeshLink components.
/// This method also finds Prefab Variants which has removed OffMeshLink components.
/// </summary>
/// <param name="searchInFolders">Folders to search for prefabs and scenes. If null, the whole project is searched.</param>
/// <returns>List of asset GUIDs to convert.</returns>
public static List<string> FindObjectsToConvert(string[] searchInFolders = null)
{
var prefabGuids = AssetDatabase.FindAssets("t:Prefab", searchInFolders);
var objectsToConvert = new HashSet<string>();
foreach (var guid in prefabGuids)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(guid));
if (prefab.GetComponentsInChildren<OffMeshLink>(true).Length > 0)
objectsToConvert.Add(guid);
var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(prefab);
if (isPrefabVariant)
{
var removedComponents = PrefabUtility.GetRemovedComponents(prefab);
foreach (var removedComponent in removedComponents)
{
if (removedComponent.assetComponent is OffMeshLink)
{
objectsToConvert.Add(guid);
break;
}
}
}
}
var sceneGuids = AssetDatabase.FindAssets("t:Scene", searchInFolders);
foreach (var guid in sceneGuids)
{
var scenePath = AssetDatabase.GUIDToAssetPath(guid);
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
var rootGameObjects = scene.GetRootGameObjects();
foreach (var rootGameObject in rootGameObjects)
{
var offMeshLinksInHierarchy = rootGameObject.GetComponentsInChildren<OffMeshLink>(true);
if (offMeshLinksInHierarchy.Length == 0)
continue;
var offMeshLinks = FindAllOffMeshLinksInHierarchy(rootGameObject);
if (offMeshLinks.Count > 0)
{
objectsToConvert.Add(guid);
break;
}
}
CloseScene(scene);
}
var returnList = new List<string>(objectsToConvert);
return returnList;
}
static void CloseScene(Scene scene)
{
// EditorSceneManager does not support closing the last scene, so we open a new empty scene instead.
if (EditorSceneManager.loadedSceneCount == 1)
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
else
EditorSceneManager.CloseScene(scene, true);
}
/// <summary>
/// Convert all objects in the list to use NavMeshLink instead of OffMeshLink.
/// </summary>
/// <param name="objectsToConvert">List of asset GUIDs to convert.</param>
/// <param name="failedConversions">List of failed conversions.</param>
public static void Convert(List<string> objectsToConvert, out List<FailedConversion> failedConversions)
{
failedConversions = new List<FailedConversion>();
var offMeshLinkToNavMeshLink = new Dictionary<OffMeshLink, NavMeshLink>();
var failedToConvert = new HashSet<string>();
// Initial sorting of objects to convert.
// We want to deal with source prefabs first, then their variants, so that the NavMeshLink components are created in the correct order.
SortAssetsToConvert(objectsToConvert);
// First convert, create NavMeshLink with OffMeshLink values
foreach (var guid in objectsToConvert)
{
var pathToObject = AssetDatabase.GUIDToAssetPath(guid);
if (!DoesAssetExistOnDisk(pathToObject))
{
failedToConvert.Add(guid);
failedConversions.Add(new FailedConversion
{
itemIndex = objectsToConvert.IndexOf(guid),
failureMessage = $"Cannot find the asset at path {pathToObject}. Please make sure the file exists."
});
continue;
}
if (!CanWriteToAsset(pathToObject))
{
failedToConvert.Add(guid);
failedConversions.Add(new FailedConversion
{
itemIndex = objectsToConvert.IndexOf(guid),
failureMessage = $"Cannot write to the asset at path {pathToObject}. Please make sure the file is not read-only."
});
continue;
}
if (TryGetPrefabFromPath(pathToObject, out var prefab))
ConvertPrefab(prefab, offMeshLinkToNavMeshLink);
else if (TryGetSceneFromPath(pathToObject, out var scene))
{
ConvertScene(scene, ref offMeshLinkToNavMeshLink);
CloseScene(scene);
}
}
// Remove failed conversions
foreach (var guid in failedToConvert)
objectsToConvert.Remove(guid);
// Second convert, apply prefab overrides to NavMeshLink
foreach (var guid in objectsToConvert)
{
var pathToObject = AssetDatabase.GUIDToAssetPath(guid);
if (TryGetPrefabFromPath(pathToObject, out var prefab))
{
ApplyOverrideDataToPrefab(prefab, offMeshLinkToNavMeshLink);
SyncOverriddenRemovalsOfComponents(prefab, offMeshLinkToNavMeshLink);
}
else if (TryGetSceneFromPath(pathToObject, out var scene))
{
ApplyOverrideDataToScene(scene, offMeshLinkToNavMeshLink);
CloseScene(scene);
}
}
// Remove OffMeshLinks from the objects
foreach (var guid in objectsToConvert)
{
var pathToObject = AssetDatabase.GUIDToAssetPath(guid);
if (TryGetPrefabFromPath(pathToObject, out var prefab))
{
var offMeshLinks = prefab.GetComponentsInChildren<OffMeshLink>(true);
foreach (var offMeshLink in offMeshLinks)
Object.DestroyImmediate(offMeshLink, true);
// Since we are removing components in source prefabs, we have to "revert removed components"
// on prefab instances, so that we don't store the removal data in the meta files.
if (PrefabUtility.IsAnyPrefabInstanceRoot(prefab))
RevertRemovedComponent(prefab);
PrefabUtility.SavePrefabAsset(prefab);
}
else if (TryGetSceneFromPath(pathToObject, out var scene))
{
var rootGameObjects = scene.GetRootGameObjects();
foreach (var rootGameObject in rootGameObjects)
{
var offMeshLinks = rootGameObject.GetComponentsInChildren<OffMeshLink>(true);
foreach (var offMeshLink in offMeshLinks)
Object.DestroyImmediate(offMeshLink, true);
}
EditorSceneManager.SaveScene(scene);
CloseScene(scene);
}
}
}
/// <summary>
/// Sorts the list of objects to convert so that source prefabs are first.
/// </summary>
/// <param name="objectsToConvert">List of asset GUIDs to convert.</param>
static void SortAssetsToConvert(List<string> objectsToConvert)
{
var sortedAssetGuids = new List<string>(objectsToConvert.Count);
for (var i = 0; i < objectsToConvert.Count; i++)
{
var assetGuid = objectsToConvert[i];
if (sortedAssetGuids.Contains(assetGuid))
continue;
var prefabGameObject = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(assetGuid));
if (prefabGameObject == null)
sortedAssetGuids.Add(assetGuid);
else
SortPrefabsToConvert(sortedAssetGuids, prefabGameObject, assetGuid);
}
objectsToConvert.Clear();
objectsToConvert.AddRange(sortedAssetGuids);
}
static void SortPrefabsToConvert(List<string> objectsToConvert, GameObject prefabRoot, string prefabRootGuid)
{
if (objectsToConvert.Contains(prefabRootGuid))
return;
var source = PrefabUtility.GetCorrespondingObjectFromSource(prefabRoot);
if (source == null)
{
objectsToConvert.Add(prefabRootGuid);
return;
}
var sourceGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(source));
SortPrefabsToConvert(objectsToConvert, source, sourceGuid);
objectsToConvert.Add(prefabRootGuid);
}
static void RevertRemovedComponent(GameObject prefab)
{
var removedComponents = PrefabUtility.GetRemovedComponents(prefab);
if (removedComponents == null)
return;
foreach (var removedComponent in removedComponents)
{
if (removedComponent.assetComponent is OffMeshLink)
PrefabUtility.RevertRemovedComponent(prefab, removedComponent.assetComponent, InteractionMode.AutomatedAction);
}
}
static bool TryGetPrefabFromPath(string path, out GameObject prefab)
{
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
return prefab != null && PrefabUtility.IsPartOfPrefabAsset(prefab);
}
static bool TryGetSceneFromPath(string path, out Scene scene)
{
var sceneAsset = AssetDatabase.LoadAssetAtPath<SceneAsset>(path);
if (sceneAsset != null)
{
scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
return true;
}
scene = default;
return false;
}
static List<OffMeshLink> FindAllOffMeshLinksInHierarchy(GameObject rootGameObject)
{
var arr = rootGameObject.GetComponentsInChildren<OffMeshLink>(true);
var offMeshLinks = new List<OffMeshLink>(arr);
for (var i = offMeshLinks.Count - 1; i >= 0; i--)
{
var gameObject = offMeshLinks[i].gameObject;
if (PrefabUtility.IsAnyPrefabInstanceRoot(gameObject) && gameObject != rootGameObject)
offMeshLinks.RemoveAt(i);
}
return offMeshLinks;
}
static void ConvertPrefab(GameObject prefabRoot, Dictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var offMeshLinks = FindAllOffMeshLinksInHierarchy(prefabRoot);
var updatedPrefab = false;
foreach (var offMeshLink in offMeshLinks)
{
var gameObject = offMeshLink.gameObject;
var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(gameObject);
// If the prefab is a variant and the OffMeshLink is not an override,
// only store the variant's OffMeshLink and NavMeshLink pair, and skip the rest of the conversion.
if (isPrefabVariant && !PrefabUtility.IsAddedComponentOverride(offMeshLink))
{
var sourceOml = PrefabUtility.GetCorrespondingObjectFromSource(offMeshLink);
var sourceNML = offMeshLinkToNavMeshLink[sourceOml];
var variantNML = GetCorrespondingVariantComponent(sourceNML, prefabRoot);
offMeshLinkToNavMeshLink.Add(offMeshLink, variantNML);
continue;
}
var navMeshLink = gameObject.AddComponent<NavMeshLink>();
offMeshLinkToNavMeshLink.Add(offMeshLink, navMeshLink);
// If the OffMeshLink is an override (created by a Prefab Variant), record the new NavMeshLink as an override.
if (isPrefabVariant)
PrefabUtility.RecordPrefabInstancePropertyModifications(gameObject);
CopyValues(offMeshLink, navMeshLink);
updatedPrefab = true;
}
if (updatedPrefab)
PrefabUtility.SavePrefabAsset(prefabRoot);
}
static void ConvertScene(Scene scene, ref Dictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var rootGameObjects = scene.GetRootGameObjects();
foreach (var rootGameObject in rootGameObjects)
{
// Skip prefab instances, they are taken care of in the ConvertPrefab method.
if (PrefabUtility.IsAnyPrefabInstanceRoot(rootGameObject))
continue;
var offMeshLinks = FindAllOffMeshLinksInHierarchy(rootGameObject);
foreach (var offMeshLink in offMeshLinks)
{
var navMeshLink = offMeshLink.gameObject.AddComponent<NavMeshLink>();
offMeshLinkToNavMeshLink.Add(offMeshLink, navMeshLink);
CopyValues(offMeshLink, navMeshLink);
}
}
EditorSceneManager.SaveScene(scene);
}
static bool CanWriteToAsset(string filePath)
{
var fileInfo = new System.IO.FileInfo(filePath);
return !fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReadOnly);
}
static bool DoesAssetExistOnDisk(string filePath)
{
return !string.IsNullOrEmpty(filePath) && System.IO.File.Exists(filePath);
}
static void CopyValues(OffMeshLink offMeshLink, NavMeshLink navMeshLink)
{
navMeshLink.activated = offMeshLink.activated;
navMeshLink.autoUpdate = offMeshLink.autoUpdatePositions;
navMeshLink.bidirectional = offMeshLink.biDirectional;
navMeshLink.costModifier = offMeshLink.costOverride;
navMeshLink.startTransform = offMeshLink.startTransform;
navMeshLink.endTransform = offMeshLink.endTransform;
}
/// <summary>
/// Transfers any existing override data from OffMeshLink components to NavMeshLink components.
/// </summary>
/// <param name="outerPrefabRoot">The out-most prefab root</param>
/// <param name="offMeshLinkToNavMeshLink">Dictionary storing the OffMeshLink and NavMeshLink pairs</param>
static void ApplyOverrideDataToPrefab(GameObject outerPrefabRoot, Dictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var didUpdate = false;
var updatedRoots = new HashSet<GameObject>();
var offMeshLinks = outerPrefabRoot.GetComponentsInChildren<OffMeshLink>(true);
foreach (var offMeshLink in offMeshLinks)
{
// Find the prefab root and check if we have already updated it.
// A null root means that the root is the outerPrefabRoot.
var rootGameObject = PrefabUtility.GetNearestPrefabInstanceRoot(offMeshLink.gameObject);
rootGameObject = rootGameObject == null ? offMeshLink.gameObject : rootGameObject;
if (updatedRoots.Contains(rootGameObject))
continue;
var propertyModifications = PrefabUtility.GetPropertyModifications(rootGameObject);
if (propertyModifications == null)
continue;
UpdatePropertyModifications(ref propertyModifications, offMeshLinkToNavMeshLink);
PrefabUtility.SetPropertyModifications(rootGameObject, propertyModifications);
updatedRoots.Add(rootGameObject);
didUpdate = true;
}
if (didUpdate)
PrefabUtility.SavePrefabAsset(outerPrefabRoot);
}
/// <summary>
/// Removes the NavMeshLink components that correspond to OffMeshLink components that were removed from the variant prefab.
/// </summary>
static void SyncOverriddenRemovalsOfComponents(GameObject prefabRoot, IReadOnlyDictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var isPrefabVariant = PrefabUtility.IsPartOfVariantPrefab(prefabRoot);
if (!isPrefabVariant)
return;
var removedComponents = PrefabUtility.GetRemovedComponents(prefabRoot);
var variantNavMeshLinks = prefabRoot.GetComponents<NavMeshLink>();
foreach (var removedComponent in removedComponents)
{
if (removedComponent.assetComponent is not OffMeshLink removedOffMeshLink)
continue;
var sourceNavMeshLink = offMeshLinkToNavMeshLink[removedOffMeshLink];
foreach(var variantComponent in variantNavMeshLinks)
{
var correspondingComponent = PrefabUtility.GetCorrespondingObjectFromSource(variantComponent);
if (correspondingComponent == sourceNavMeshLink)
{
Object.DestroyImmediate(variantComponent, true);
break;
}
}
}
PrefabUtility.RecordPrefabInstancePropertyModifications(prefabRoot);
PrefabUtility.SavePrefabAsset(prefabRoot);
}
static void ApplyOverrideDataToScene(Scene scene, Dictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var rootGameObjects = scene.GetRootGameObjects();
var updatedRoots = new HashSet<GameObject>();
foreach (var rootGameObject in rootGameObjects)
{
var offMeshLinks = rootGameObject.GetComponentsInChildren<OffMeshLink>();
foreach (var offMeshLink in offMeshLinks)
{
// Non-prefab instances cannot have overrides, so continue.
if (!PrefabUtility.IsPartOfAnyPrefab(offMeshLink))
continue;
// Find the prefab root and check if it contains any overrides
var prefabRoot = PrefabUtility.GetNearestPrefabInstanceRoot(offMeshLink.gameObject);
if (!PrefabUtility.HasPrefabInstanceAnyOverrides(prefabRoot, false) && !updatedRoots.Contains(prefabRoot))
continue;
var propertyModifications = PrefabUtility.GetPropertyModifications(prefabRoot);
UpdatePropertyModifications(ref propertyModifications, offMeshLinkToNavMeshLink);
PrefabUtility.SetPropertyModifications(prefabRoot, propertyModifications);
updatedRoots.Add(prefabRoot);
}
}
EditorSceneManager.SaveScene(scene);
}
static void UpdatePropertyModifications(ref PropertyModification[] propertyModifications, Dictionary<OffMeshLink, NavMeshLink> offMeshLinkToNavMeshLink)
{
var updatedModifications = new List<PropertyModification>(propertyModifications);
for (var i = 0; i < propertyModifications.Length; ++i)
{
var modification = propertyModifications[i];
// Skip if the target is not an OffMeshLink
if (modification.target is not OffMeshLink oml)
{
updatedModifications.Add(modification);
continue;
}
// Get the NavMeshLink that corresponds to the OffMeshLink.
var navMeshLink = offMeshLinkToNavMeshLink[oml];
var newMod = new PropertyModification
{
target = navMeshLink,
value = modification.value
};
switch (modification.propertyPath)
{
case "m_AutoUpdatePositions":
newMod.propertyPath = "m_AutoUpdatePosition";
break;
case "m_CostOverride":
newMod.propertyPath = "m_CostModifier";
if (float.TryParse(newMod.value, out var floatVal))
{
if (floatVal > 0)
{
var overrideMod = new PropertyModification()
{
target = navMeshLink,
propertyPath = "m_IsOverridingCost",
value = "1"
};
updatedModifications.Add(overrideMod);
}
}
break;
case "m_BiDirectional":
newMod.propertyPath = "m_Bidirectional";
break;
case "m_Start":
newMod.propertyPath = "m_StartTransform";
break;
case "m_End":
newMod.propertyPath = "m_EndTransform";
break;
case "m_Activated":
newMod.propertyPath = modification.propertyPath;
break;
}
updatedModifications.Add(newMod);
}
propertyModifications = updatedModifications.ToArray();
}
/// <summary>
/// Get the corresponding variant component of a source prefab component.
/// </summary>
/// <param name="component">The source prefab component</param>
/// <param name="variantPrefab">The variant prefab</param>
/// <typeparam name="TComponent">The type of component to search for</typeparam>
/// <returns>Returns the corresponding component on the variant of the source prefab component, if found, or null.</returns>
static TComponent GetCorrespondingVariantComponent<TComponent>(TComponent component, GameObject variantPrefab) where TComponent : Component
{
var variantComponents = variantPrefab.GetComponents(component.GetType());
foreach (var variantComponent in variantComponents)
{
var correspondingComponent = PrefabUtility.GetCorrespondingObjectFromSource(variantComponent);
if (correspondingComponent == component)
return variantComponent as TComponent;
}
return null;
}
}
#pragma warning restore 618
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e2b338e146db4a408444f723bd4a2c77
timeCreated: 1710247263

View File

@@ -0,0 +1,20 @@
{
"name": "Unity.AI.Navigation.Updater",
"rootNamespace": "",
"references": [
"GUID:8c4dd21966739024fbd72155091d199e",
"GUID:86c9d8e67265f41469be06142c397d17",
"GUID:c57630adab7a340ec94f10e4fcac12f1"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1664e92176d434ccd902c1705fefe682
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: