1154 lines
43 KiB
C#
1154 lines
43 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System;
|
||
|
||
public class BatchRandomizerWindow : EditorWindow
|
||
{
|
||
private List<GameObject> selectedObjects = new List<GameObject>();
|
||
private List<SerializedObject> serializedObjects = new List<SerializedObject>();
|
||
private Dictionary<GameObject, List<Component>> objectComponents = new Dictionary<GameObject, List<Component>>();
|
||
private bool includeChildren = true;
|
||
private Vector2 propertiesScrollPosition;
|
||
private Vector2 selectedPropertiesScrollPosition;
|
||
private Dictionary<string, bool> propertyFoldouts = new Dictionary<string, bool>();
|
||
private Dictionary<string, bool> componentFoldouts = new Dictionary<string, bool>();
|
||
private Dictionary<string, PropertyRange> propertyRanges = new Dictionary<string, PropertyRange>();
|
||
private Dictionary<string, bool> selectedProperties = new Dictionary<string, bool>();
|
||
private bool showOnlyCommonProperties = true;
|
||
private int propertySelectionMode = 1; // 0 = GameObject, 1 = Components (now default)
|
||
private string searchText = "";
|
||
|
||
// Track property paths across objects to find common ones
|
||
private Dictionary<string, int> propertyOccurrences = new Dictionary<string, int>();
|
||
private HashSet<string> allPropertyPaths = new HashSet<string>();
|
||
private List<string> selectedPropertyList = new List<string>();
|
||
|
||
// Used to track expanded paths in the UI
|
||
private HashSet<string> expandedPaths = new HashSet<string>();
|
||
|
||
// Component-specific properties
|
||
private Dictionary<Type, List<SerializedObject>> componentsByType = new Dictionary<Type, List<SerializedObject>>();
|
||
|
||
[MenuItem("Tools/Batch Property Randomizer")]
|
||
public static void ShowWindow()
|
||
{
|
||
GetWindow<BatchRandomizerWindow>("Batch Randomizer");
|
||
}
|
||
|
||
private void OnSelectionChange()
|
||
{
|
||
RefreshSelectedObjects();
|
||
Repaint();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
RefreshSelectedObjects();
|
||
}
|
||
|
||
private void RefreshSelectedObjects()
|
||
{
|
||
selectedObjects.Clear();
|
||
serializedObjects.Clear();
|
||
propertyOccurrences.Clear();
|
||
allPropertyPaths.Clear();
|
||
objectComponents.Clear();
|
||
componentsByType.Clear();
|
||
|
||
// Keep previously selected properties
|
||
HashSet<string> previouslySelectedProperties = new HashSet<string>();
|
||
foreach (var kvp in selectedProperties)
|
||
{
|
||
if (kvp.Value)
|
||
previouslySelectedProperties.Add(kvp.Key);
|
||
}
|
||
selectedProperties.Clear();
|
||
|
||
// Get currently selected GameObjects
|
||
foreach (GameObject obj in Selection.gameObjects)
|
||
{
|
||
selectedObjects.Add(obj);
|
||
serializedObjects.Add(new SerializedObject(obj));
|
||
|
||
// Add components for this GameObject
|
||
objectComponents[obj] = new List<Component>();
|
||
CollectComponentsForObject(obj);
|
||
|
||
if (includeChildren)
|
||
{
|
||
// Add all children
|
||
Transform[] childTransforms = obj.GetComponentsInChildren<Transform>(true);
|
||
foreach (Transform childTransform in childTransforms)
|
||
{
|
||
if (childTransform.gameObject == obj)
|
||
continue; // Skip the parent
|
||
|
||
selectedObjects.Add(childTransform.gameObject);
|
||
serializedObjects.Add(new SerializedObject(childTransform.gameObject));
|
||
|
||
// Add components for this child
|
||
objectComponents[childTransform.gameObject] = new List<Component>();
|
||
CollectComponentsForObject(childTransform.gameObject);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Find common properties for GameObject mode
|
||
foreach (SerializedObject serializedObject in serializedObjects)
|
||
{
|
||
var iteratorObj = serializedObject.GetIterator();
|
||
iteratorObj.Next(true); // Skip first property (script)
|
||
|
||
// First collect all property paths
|
||
while (iteratorObj.NextVisible(true))
|
||
{
|
||
if (!IsRandomizableProperty(iteratorObj))
|
||
continue;
|
||
|
||
string path = iteratorObj.propertyPath;
|
||
allPropertyPaths.Add(path);
|
||
|
||
if (!propertyOccurrences.ContainsKey(path))
|
||
propertyOccurrences[path] = 0;
|
||
propertyOccurrences[path]++;
|
||
}
|
||
}
|
||
|
||
// Find common properties for Component mode
|
||
foreach (var componentType in componentsByType.Keys)
|
||
{
|
||
var componentSerializedObjects = componentsByType[componentType];
|
||
if (componentSerializedObjects.Count == 0)
|
||
continue;
|
||
|
||
// Take the first one to get paths
|
||
var firstObj = componentSerializedObjects[0];
|
||
var iterator = firstObj.GetIterator();
|
||
iterator.Next(true);
|
||
|
||
while (iterator.NextVisible(true))
|
||
{
|
||
if (!IsRandomizableProperty(iterator))
|
||
continue;
|
||
|
||
string path = $"{componentType.Name}:{iterator.propertyPath}";
|
||
allPropertyPaths.Add(path);
|
||
|
||
if (!propertyOccurrences.ContainsKey(path))
|
||
propertyOccurrences[path] = 0;
|
||
|
||
// Count how many components have this property
|
||
foreach (var componentObj in componentSerializedObjects)
|
||
{
|
||
var prop = componentObj.FindProperty(iterator.propertyPath);
|
||
if (prop != null)
|
||
{
|
||
propertyOccurrences[path]++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Restore previously selected properties if they still exist
|
||
foreach (var path in allPropertyPaths)
|
||
{
|
||
if (previouslySelectedProperties.Contains(path))
|
||
{
|
||
selectedProperties[path] = true;
|
||
if (!selectedPropertyList.Contains(path))
|
||
{
|
||
selectedPropertyList.Add(path);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
selectedProperties[path] = false;
|
||
}
|
||
}
|
||
|
||
// Clean up selected property list (remove properties that no longer exist)
|
||
selectedPropertyList = selectedPropertyList.Where(p => allPropertyPaths.Contains(p)).ToList();
|
||
}
|
||
|
||
private void CollectComponentsForObject(GameObject obj)
|
||
{
|
||
Component[] components = obj.GetComponents<Component>();
|
||
objectComponents[obj].AddRange(components);
|
||
|
||
foreach (var component in components)
|
||
{
|
||
if (component == null) continue;
|
||
|
||
Type componentType = component.GetType();
|
||
|
||
if (!componentsByType.ContainsKey(componentType))
|
||
{
|
||
componentsByType[componentType] = new List<SerializedObject>();
|
||
}
|
||
|
||
componentsByType[componentType].Add(new SerializedObject(component));
|
||
}
|
||
}
|
||
|
||
private string GetDisplayNameForProperty(string propertyPath)
|
||
{
|
||
// For component properties (Component:Property format)
|
||
if (propertyPath.Contains(":"))
|
||
{
|
||
string[] parts = propertyPath.Split(':');
|
||
return $"{parts[0]}.{GetPropertyDisplayName(parts[1])}";
|
||
}
|
||
|
||
return GetPropertyDisplayName(propertyPath);
|
||
}
|
||
|
||
void OnGUI()
|
||
{
|
||
EditorGUILayout.LabelField("Batch Property Randomizer", EditorStyles.boldLabel);
|
||
|
||
// Include children toggle
|
||
bool newIncludeChildren = EditorGUILayout.Toggle("Include Children", includeChildren);
|
||
if (newIncludeChildren != includeChildren)
|
||
{
|
||
includeChildren = newIncludeChildren;
|
||
RefreshSelectedObjects();
|
||
}
|
||
|
||
// Only show common properties toggle
|
||
showOnlyCommonProperties = EditorGUILayout.Toggle("Only Common Properties", showOnlyCommonProperties);
|
||
|
||
// Property selection mode toggle with "Component Properties" as the first tab
|
||
string[] modes = { "Component Properties", "GameObject Properties" };
|
||
int newMode = GUILayout.Toolbar(propertySelectionMode, modes);
|
||
if (newMode != propertySelectionMode)
|
||
{
|
||
propertySelectionMode = newMode;
|
||
searchText = ""; // Clear search when switching modes
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// Display selected objects count
|
||
if (selectedObjects.Count > 0)
|
||
{
|
||
EditorGUILayout.LabelField($"Selected Objects: {selectedObjects.Count}", EditorStyles.boldLabel);
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.HelpBox("No objects selected. Please select GameObjects in the scene.", MessageType.Info);
|
||
return;
|
||
}
|
||
|
||
// Layout for searchable property list and selected properties panel
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// Left panel - searchable property list
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f));
|
||
DrawPropertySearchPanel();
|
||
EditorGUILayout.EndVertical();
|
||
|
||
// Right panel - selected properties for editing
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f));
|
||
DrawSelectedPropertiesPanel();
|
||
EditorGUILayout.EndVertical();
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Space();
|
||
|
||
// Randomize button at the bottom
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button("Randomize Selected Properties", GUILayout.Height(30)))
|
||
{
|
||
RandomizeProperties();
|
||
}
|
||
}
|
||
|
||
private void DrawPropertySearchPanel()
|
||
{
|
||
EditorGUILayout.LabelField("Available Properties", EditorStyles.boldLabel);
|
||
|
||
// Search bar
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUI.BeginChangeCheck();
|
||
searchText = EditorGUILayout.TextField("Search", searchText, EditorStyles.toolbarSearchField);
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
// Reset foldouts when search changes to show all matching results
|
||
if (!string.IsNullOrEmpty(searchText))
|
||
{
|
||
foreach (var key in propertyFoldouts.Keys.ToList())
|
||
{
|
||
propertyFoldouts[key] = true;
|
||
}
|
||
foreach (var key in componentFoldouts.Keys.ToList())
|
||
{
|
||
componentFoldouts[key] = true;
|
||
}
|
||
}
|
||
}
|
||
if (GUILayout.Button("Clear", EditorStyles.miniButton, GUILayout.Width(60)))
|
||
{
|
||
searchText = "";
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// Property list
|
||
propertiesScrollPosition = EditorGUILayout.BeginScrollView(propertiesScrollPosition, EditorStyles.helpBox);
|
||
|
||
if (propertySelectionMode == 0) // GameObject properties (second tab)
|
||
{
|
||
DrawGameObjectPropertiesList();
|
||
}
|
||
else // Component properties (first/default tab)
|
||
{
|
||
DrawComponentPropertiesList();
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawSelectedPropertiesPanel()
|
||
{
|
||
EditorGUILayout.LabelField("Selected Properties", EditorStyles.boldLabel);
|
||
|
||
// Buttons for managing selected properties
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("Clear All"))
|
||
{
|
||
// Defer clearing to avoid modifying during layout
|
||
EditorApplication.delayCall += () => {
|
||
foreach (var key in selectedProperties.Keys.ToList())
|
||
{
|
||
selectedProperties[key] = false;
|
||
}
|
||
selectedPropertyList.Clear();
|
||
Repaint();
|
||
};
|
||
}
|
||
if (GUILayout.Button("Remove Selected"))
|
||
{
|
||
// Defer removal to avoid modifying during layout
|
||
EditorApplication.delayCall += () => {
|
||
List<string> toRemove = new List<string>();
|
||
foreach (var path in selectedPropertyList)
|
||
{
|
||
if (selectedProperties[path])
|
||
{
|
||
toRemove.Add(path);
|
||
selectedProperties[path] = false;
|
||
}
|
||
}
|
||
foreach (var path in toRemove)
|
||
{
|
||
selectedPropertyList.Remove(path);
|
||
}
|
||
Repaint();
|
||
};
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// Selected properties list with editing fields
|
||
selectedPropertiesScrollPosition = EditorGUILayout.BeginScrollView(selectedPropertiesScrollPosition, EditorStyles.helpBox);
|
||
|
||
if (selectedPropertyList.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("No properties selected. Click '+' next to properties in the list on the left to add them here.", MessageType.Info);
|
||
}
|
||
else
|
||
{
|
||
// Store properties to remove after the loop to avoid modifying during layout
|
||
List<string> propertiesToRemove = new List<string>();
|
||
|
||
for (int i = 0; i < selectedPropertyList.Count; i++)
|
||
{
|
||
string path = selectedPropertyList[i];
|
||
if (!propertyRanges.ContainsKey(path))
|
||
{
|
||
propertyRanges[path] = GetDefaultPropertyRange(path, null);
|
||
}
|
||
|
||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
// Checkbox for selecting/deselecting for batch operations
|
||
bool isSelected = EditorGUILayout.ToggleLeft(GetDisplayNameForProperty(path), selectedProperties[path], EditorStyles.boldLabel);
|
||
if (isSelected != selectedProperties[path])
|
||
{
|
||
selectedProperties[path] = isSelected;
|
||
}
|
||
|
||
// Remove button - Instead of removing immediately, add to list for delayed removal
|
||
if (GUILayout.Button("×", EditorStyles.miniButton, GUILayout.Width(20)))
|
||
{
|
||
propertiesToRemove.Add(path);
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
PropertyRange range = propertyRanges[path];
|
||
EditorGUI.indentLevel++;
|
||
DrawPropertyRangeFields(path, range);
|
||
EditorGUI.indentLevel--;
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space();
|
||
}
|
||
|
||
// Process removals after the loop is complete
|
||
if (propertiesToRemove.Count > 0)
|
||
{
|
||
EditorApplication.delayCall += () => {
|
||
foreach (string path in propertiesToRemove)
|
||
{
|
||
selectedPropertyList.Remove(path);
|
||
selectedProperties[path] = false;
|
||
}
|
||
Repaint();
|
||
};
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawComponentPropertiesList()
|
||
{
|
||
bool hasMatchingProperties = false;
|
||
|
||
// Display properties in a flat list for easier searching
|
||
List<PropertyListItem> propertyItems = new List<PropertyListItem>();
|
||
|
||
// Collect all properties from all component types
|
||
foreach (var typeEntry in componentsByType.OrderBy(entry => entry.Key.Name))
|
||
{
|
||
Type componentType = typeEntry.Key;
|
||
List<SerializedObject> componentObjects = typeEntry.Value;
|
||
|
||
// Skip if no components
|
||
if (componentObjects.Count == 0)
|
||
continue;
|
||
|
||
string typeName = componentType.Name;
|
||
|
||
// Get properties from the first component
|
||
var firstComponent = componentObjects[0];
|
||
var iterator = firstComponent.GetIterator();
|
||
iterator.Next(true);
|
||
|
||
// Track visible properties to avoid duplicates
|
||
HashSet<string> displayedProps = new HashSet<string>();
|
||
|
||
while (iterator.NextVisible(true))
|
||
{
|
||
if (!IsRandomizableProperty(iterator) || displayedProps.Contains(iterator.propertyPath))
|
||
continue;
|
||
|
||
displayedProps.Add(iterator.propertyPath);
|
||
string fullPath = $"{componentType.Name}:{iterator.propertyPath}";
|
||
|
||
// Skip non-common properties if filter is enabled
|
||
if (showOnlyCommonProperties && !IsCommonProperty(fullPath))
|
||
continue;
|
||
|
||
string propName = GetPropertyDisplayName(iterator.propertyPath);
|
||
|
||
// Add to our flat list
|
||
propertyItems.Add(new PropertyListItem {
|
||
Path = fullPath,
|
||
DisplayName = $"{typeName}.{propName}",
|
||
ComponentName = typeName
|
||
});
|
||
}
|
||
}
|
||
|
||
// Filter by search text if needed
|
||
if (!string.IsNullOrEmpty(searchText))
|
||
{
|
||
propertyItems = propertyItems
|
||
.Where(item =>
|
||
item.DisplayName.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
.ToList();
|
||
}
|
||
|
||
// Sort the list by component name, then property name
|
||
propertyItems = propertyItems
|
||
.OrderBy(item => item.ComponentName)
|
||
.ThenBy(item => item.DisplayName)
|
||
.ToList();
|
||
|
||
if (propertyItems.Count > 0)
|
||
{
|
||
hasMatchingProperties = true;
|
||
|
||
// Show header with count
|
||
EditorGUILayout.LabelField($"Properties ({propertyItems.Count})", EditorStyles.boldLabel);
|
||
|
||
// Draw the flat list of properties
|
||
foreach (var item in propertyItems)
|
||
{
|
||
DrawPropertyListItem(item.Path, item.DisplayName);
|
||
}
|
||
}
|
||
|
||
if (!hasMatchingProperties)
|
||
{
|
||
EditorGUILayout.HelpBox($"No properties match '{searchText}'", MessageType.Info);
|
||
}
|
||
}
|
||
|
||
private void DrawGameObjectPropertiesList()
|
||
{
|
||
bool hasMatchingProperties = false;
|
||
|
||
// Use a flat list for GameObject properties as well
|
||
List<PropertyListItem> propertyItems = new List<PropertyListItem>();
|
||
|
||
// Group properties by component for organization
|
||
Dictionary<string, List<string>> componentProperties = GroupPropertiesByComponent();
|
||
|
||
foreach (var componentEntry in componentProperties)
|
||
{
|
||
string componentName = componentEntry.Key;
|
||
List<string> properties = componentEntry.Value;
|
||
|
||
// Skip if no properties for this component
|
||
if (properties.Count == 0)
|
||
continue;
|
||
|
||
foreach (string path in properties)
|
||
{
|
||
// Skip non-common properties if filter is enabled
|
||
if (showOnlyCommonProperties && !IsCommonProperty(path))
|
||
continue;
|
||
|
||
string propertyName = GetPropertyDisplayName(path);
|
||
|
||
// Add to our flat list
|
||
propertyItems.Add(new PropertyListItem {
|
||
Path = path,
|
||
DisplayName = $"{componentName}.{propertyName}",
|
||
ComponentName = componentName
|
||
});
|
||
}
|
||
}
|
||
|
||
// Filter by search text if needed
|
||
if (!string.IsNullOrEmpty(searchText))
|
||
{
|
||
propertyItems = propertyItems
|
||
.Where(item =>
|
||
item.DisplayName.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
.ToList();
|
||
}
|
||
|
||
// Sort the list by component name, then property name
|
||
propertyItems = propertyItems
|
||
.OrderBy(item => item.ComponentName)
|
||
.ThenBy(item => item.DisplayName)
|
||
.ToList();
|
||
|
||
if (propertyItems.Count > 0)
|
||
{
|
||
hasMatchingProperties = true;
|
||
|
||
// Show header with count
|
||
EditorGUILayout.LabelField($"Properties ({propertyItems.Count})", EditorStyles.boldLabel);
|
||
|
||
// Draw the flat list of properties
|
||
foreach (var item in propertyItems)
|
||
{
|
||
DrawPropertyListItem(item.Path, item.DisplayName);
|
||
}
|
||
}
|
||
|
||
if (!hasMatchingProperties)
|
||
{
|
||
EditorGUILayout.HelpBox($"No properties match '{searchText}'", MessageType.Info);
|
||
}
|
||
}
|
||
|
||
private void DrawPropertyListItem(string path, string displayName)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// Property name
|
||
EditorGUILayout.LabelField(displayName);
|
||
|
||
// Add button - defer addition to avoid layout issues
|
||
GUI.enabled = !selectedPropertyList.Contains(path);
|
||
if (GUILayout.Button("+", EditorStyles.miniButton, GUILayout.Width(20)))
|
||
{
|
||
EditorApplication.delayCall += () => {
|
||
if (!selectedPropertyList.Contains(path))
|
||
{
|
||
selectedPropertyList.Add(path);
|
||
selectedProperties[path] = true;
|
||
Repaint();
|
||
}
|
||
};
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
// Helper class for property list items
|
||
private class PropertyListItem
|
||
{
|
||
public string Path;
|
||
public string DisplayName;
|
||
public string ComponentName;
|
||
}
|
||
|
||
private Dictionary<string, List<string>> GroupPropertiesByComponent()
|
||
{
|
||
Dictionary<string, List<string>> result = new Dictionary<string, List<string>>();
|
||
|
||
foreach (string path in allPropertyPaths)
|
||
{
|
||
// Skip component-specific paths in GameObject mode
|
||
if (path.Contains(":"))
|
||
continue;
|
||
|
||
string componentName = ExtractComponentName(path);
|
||
|
||
if (!result.ContainsKey(componentName))
|
||
result[componentName] = new List<string>();
|
||
|
||
result[componentName].Add(path);
|
||
}
|
||
|
||
// Sort each list by property name
|
||
foreach (var key in result.Keys.ToList())
|
||
{
|
||
result[key] = result[key].OrderBy(p => p).ToList();
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private string ExtractComponentName(string propertyPath)
|
||
{
|
||
// Handle component-specific paths
|
||
if (propertyPath.Contains(":"))
|
||
{
|
||
return propertyPath.Split(':')[0];
|
||
}
|
||
|
||
// Paths typically look like "m_LocalPosition.x" or "componentName.propertyName"
|
||
string[] parts = propertyPath.Split('.');
|
||
|
||
// Special handling for common Transform properties
|
||
if (parts[0].StartsWith("m_Local"))
|
||
return "Transform";
|
||
|
||
if (parts[0] == "m_IsActive")
|
||
return "GameObject";
|
||
|
||
return parts[0];
|
||
}
|
||
|
||
private string GetPropertyDisplayName(string propertyPath)
|
||
{
|
||
// Handle component-specific paths
|
||
if (propertyPath.Contains(":"))
|
||
{
|
||
string[] splitPath = propertyPath.Split(':');
|
||
return GetPropertyDisplayName(splitPath[1]);
|
||
}
|
||
|
||
// Convert property path to more human-readable form
|
||
string[] parts = propertyPath.Split('.');
|
||
string result = parts[0];
|
||
|
||
// Handle special cases
|
||
if (parts[0].StartsWith("m_"))
|
||
{
|
||
result = parts[0].Substring(2); // Remove "m_" prefix
|
||
}
|
||
|
||
// Add sub-properties if they exist
|
||
if (parts.Length > 1)
|
||
{
|
||
result += "." + string.Join(".", parts.Skip(1));
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
private bool IsCommonProperty(string propertyPath)
|
||
{
|
||
// A property is common if it appears in all selected objects/components
|
||
if (propertyPath.Contains(":"))
|
||
{
|
||
// For component properties, check against number of components of that type
|
||
string[] parts = propertyPath.Split(':');
|
||
string typeName = parts[0];
|
||
|
||
foreach (var entry in componentsByType)
|
||
{
|
||
if (entry.Key.Name == typeName)
|
||
{
|
||
return propertyOccurrences.ContainsKey(propertyPath) &&
|
||
propertyOccurrences[propertyPath] == entry.Value.Count;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
// For GameObject properties
|
||
return propertyOccurrences.ContainsKey(propertyPath) &&
|
||
propertyOccurrences[propertyPath] == serializedObjects.Count;
|
||
}
|
||
}
|
||
|
||
private void DrawPropertyRangeFields(string propertyPath, PropertyRange range)
|
||
{
|
||
EditorGUILayout.LabelField("Range:", GUILayout.Width(45));
|
||
|
||
switch (range.Type)
|
||
{
|
||
case PropertyType.Float:
|
||
EditorGUILayout.BeginHorizontal();
|
||
range.MinFloat = EditorGUILayout.FloatField(range.MinFloat, GUILayout.Width(60));
|
||
EditorGUILayout.LabelField("to", GUILayout.Width(20));
|
||
range.MaxFloat = EditorGUILayout.FloatField(range.MaxFloat, GUILayout.Width(60));
|
||
EditorGUILayout.EndHorizontal();
|
||
break;
|
||
|
||
case PropertyType.Int:
|
||
EditorGUILayout.BeginHorizontal();
|
||
range.MinInt = EditorGUILayout.IntField(range.MinInt, GUILayout.Width(60));
|
||
EditorGUILayout.LabelField("to", GUILayout.Width(20));
|
||
range.MaxInt = EditorGUILayout.IntField(range.MaxInt, GUILayout.Width(60));
|
||
EditorGUILayout.EndHorizontal();
|
||
break;
|
||
|
||
case PropertyType.Vector2:
|
||
case PropertyType.Vector3:
|
||
case PropertyType.Vector4:
|
||
EditorGUILayout.BeginVertical();
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("Min:", GUILayout.Width(30));
|
||
range.MinVector = EditorGUILayout.Vector3Field("", range.MinVector, GUILayout.Width(180));
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("Max:", GUILayout.Width(30));
|
||
range.MaxVector = EditorGUILayout.Vector3Field("", range.MaxVector, GUILayout.Width(180));
|
||
EditorGUILayout.EndHorizontal();
|
||
EditorGUILayout.EndVertical();
|
||
break;
|
||
|
||
case PropertyType.Color:
|
||
EditorGUILayout.BeginHorizontal();
|
||
range.MinColor = EditorGUILayout.ColorField(range.MinColor, GUILayout.Width(60));
|
||
EditorGUILayout.LabelField("to", GUILayout.Width(20));
|
||
range.MaxColor = EditorGUILayout.ColorField(range.MaxColor, GUILayout.Width(60));
|
||
EditorGUILayout.EndHorizontal();
|
||
break;
|
||
|
||
case PropertyType.Bool:
|
||
range.BoolProbability = EditorGUILayout.Slider(range.BoolProbability, 0f, 1f, GUILayout.Width(150));
|
||
EditorGUILayout.LabelField("probability of true", GUILayout.Width(120));
|
||
break;
|
||
|
||
case PropertyType.Quaternion:
|
||
// Improved rotation controls
|
||
EditorGUILayout.BeginVertical();
|
||
|
||
// Display the current rotation mode
|
||
string[] rotationModes = { "Full Random", "Constrained Euler" };
|
||
range.RotationMode = (RotationMode)EditorGUILayout.Popup("Mode:", (int)range.RotationMode, rotationModes);
|
||
|
||
if (range.RotationMode == RotationMode.ConstrainedEuler)
|
||
{
|
||
// Show min/max Euler angle fields
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("Min Euler:", GUILayout.Width(70));
|
||
range.MinRotation = EditorGUILayout.Vector3Field("", range.MinRotation);
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("Max Euler:", GUILayout.Width(70));
|
||
range.MaxRotation = EditorGUILayout.Vector3Field("", range.MaxRotation);
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.HelpBox("Full random rotation will be applied using uniform distribution", MessageType.Info);
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
break;
|
||
|
||
case PropertyType.Enum:
|
||
EditorGUILayout.LabelField("Random enum value will be selected", GUILayout.Width(200));
|
||
break;
|
||
|
||
default:
|
||
EditorGUILayout.LabelField("Unsupported type for randomization");
|
||
break;
|
||
}
|
||
}
|
||
|
||
private bool IsRandomizableProperty(SerializedProperty property)
|
||
{
|
||
// Skip certain properties we don't want to randomize
|
||
if (property.propertyPath.StartsWith("m_Script") ||
|
||
property.propertyType == SerializedPropertyType.ObjectReference ||
|
||
property.propertyType == SerializedPropertyType.ExposedReference ||
|
||
property.propertyType == SerializedPropertyType.String ||
|
||
property.propertyType == SerializedPropertyType.Character ||
|
||
property.propertyType == SerializedPropertyType.ArraySize ||
|
||
property.propertyType == SerializedPropertyType.Generic ||
|
||
property.propertyType == SerializedPropertyType.Gradient ||
|
||
property.propertyType == SerializedPropertyType.FixedBufferSize)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private PropertyRange GetDefaultPropertyRange(string propertyPath, SerializedProperty property = null)
|
||
{
|
||
// Try to find the SerializedProperty if not provided
|
||
if (property == null)
|
||
{
|
||
if (propertyPath.Contains(":"))
|
||
{
|
||
// Component property
|
||
string[] parts = propertyPath.Split(':');
|
||
string typeName = parts[0];
|
||
string propPath = parts[1];
|
||
|
||
foreach (var entry in componentsByType)
|
||
{
|
||
if (entry.Key.Name == typeName && entry.Value.Count > 0)
|
||
{
|
||
property = entry.Value[0].FindProperty(propPath);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// GameObject property
|
||
foreach (SerializedObject serializedObject in serializedObjects)
|
||
{
|
||
property = serializedObject.FindProperty(propertyPath);
|
||
if (property != null)
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (property == null)
|
||
return new PropertyRange { Type = PropertyType.Unsupported };
|
||
|
||
switch (property.propertyType)
|
||
{
|
||
case SerializedPropertyType.Float:
|
||
return new PropertyRange { Type = PropertyType.Float, MinFloat = 0f, MaxFloat = 1f };
|
||
|
||
case SerializedPropertyType.Integer:
|
||
return new PropertyRange { Type = PropertyType.Int, MinInt = 0, MaxInt = 100 };
|
||
|
||
case SerializedPropertyType.Boolean:
|
||
return new PropertyRange { Type = PropertyType.Bool, BoolProbability = 0.5f };
|
||
|
||
case SerializedPropertyType.Vector2:
|
||
return new PropertyRange {
|
||
Type = PropertyType.Vector2,
|
||
MinVector = new Vector3(0, 0, 0),
|
||
MaxVector = new Vector3(1, 1, 0)
|
||
};
|
||
|
||
case SerializedPropertyType.Vector3:
|
||
// Check if this is a rotation Euler angles property
|
||
if (propertyPath.Contains("otation") || propertyPath.EndsWith("Euler"))
|
||
{
|
||
return new PropertyRange {
|
||
Type = PropertyType.Vector3,
|
||
MinVector = new Vector3(0, 0, 0),
|
||
MaxVector = new Vector3(360, 360, 360)
|
||
};
|
||
}
|
||
|
||
return new PropertyRange {
|
||
Type = PropertyType.Vector3,
|
||
MinVector = new Vector3(0, 0, 0),
|
||
MaxVector = new Vector3(1, 1, 1)
|
||
};
|
||
|
||
case SerializedPropertyType.Vector4:
|
||
return new PropertyRange {
|
||
Type = PropertyType.Vector4,
|
||
MinVector = new Vector3(0, 0, 0),
|
||
MaxVector = new Vector3(1, 1, 1)
|
||
};
|
||
|
||
case SerializedPropertyType.Quaternion:
|
||
return new PropertyRange {
|
||
Type = PropertyType.Quaternion,
|
||
RotationMode = RotationMode.FullRandom,
|
||
MinRotation = new Vector3(0, 0, 0),
|
||
MaxRotation = new Vector3(360, 360, 360)
|
||
};
|
||
|
||
case SerializedPropertyType.Color:
|
||
return new PropertyRange {
|
||
Type = PropertyType.Color,
|
||
MinColor = Color.black,
|
||
MaxColor = Color.white
|
||
};
|
||
|
||
case SerializedPropertyType.Enum:
|
||
return new PropertyRange { Type = PropertyType.Enum };
|
||
|
||
default:
|
||
return new PropertyRange { Type = PropertyType.Unsupported };
|
||
}
|
||
}
|
||
|
||
private void RandomizeProperties()
|
||
{
|
||
// Record all objects for Undo operation
|
||
List<UnityEngine.Object> objectsToRecord = new List<UnityEngine.Object>(selectedObjects);
|
||
|
||
// Also record all components for component-specific properties
|
||
if (propertySelectionMode == 1)
|
||
{
|
||
foreach (var componentList in objectComponents.Values)
|
||
{
|
||
foreach (var component in componentList)
|
||
{
|
||
if (component != null)
|
||
objectsToRecord.Add(component);
|
||
}
|
||
}
|
||
}
|
||
|
||
Undo.RecordObjects(objectsToRecord.ToArray(), "Batch Randomize Properties");
|
||
|
||
if (propertySelectionMode == 0) // GameObject properties
|
||
{
|
||
RandomizeGameObjectProperties();
|
||
}
|
||
else // Component properties
|
||
{
|
||
RandomizeComponentProperties();
|
||
}
|
||
}
|
||
|
||
private void RandomizeComponentProperties()
|
||
{
|
||
foreach (var typeEntry in componentsByType)
|
||
{
|
||
Type componentType = typeEntry.Key;
|
||
string typeName = componentType.Name;
|
||
|
||
foreach (var componentObj in typeEntry.Value)
|
||
{
|
||
bool modified = false;
|
||
|
||
foreach (string fullPath in selectedProperties.Keys)
|
||
{
|
||
if (!selectedProperties[fullPath] || !fullPath.StartsWith(typeName + ":"))
|
||
continue;
|
||
|
||
string[] parts = fullPath.Split(':');
|
||
string propertyPath = parts[1];
|
||
|
||
SerializedProperty property = componentObj.FindProperty(propertyPath);
|
||
if (property == null)
|
||
continue;
|
||
|
||
if (RandomizeProperty(property, propertyRanges[fullPath]))
|
||
modified = true;
|
||
}
|
||
|
||
if (modified)
|
||
{
|
||
componentObj.ApplyModifiedProperties();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RandomizeGameObjectProperties()
|
||
{
|
||
foreach (SerializedObject serializedObject in serializedObjects)
|
||
{
|
||
bool modified = false;
|
||
|
||
foreach (string path in selectedProperties.Keys)
|
||
{
|
||
if (!selectedProperties[path] || path.Contains(":"))
|
||
continue;
|
||
|
||
SerializedProperty property = serializedObject.FindProperty(path);
|
||
if (property == null)
|
||
continue;
|
||
|
||
if (RandomizeProperty(property, propertyRanges[path]))
|
||
modified = true;
|
||
}
|
||
|
||
if (modified)
|
||
{
|
||
serializedObject.ApplyModifiedProperties();
|
||
}
|
||
}
|
||
}
|
||
|
||
private bool RandomizeProperty(SerializedProperty property, PropertyRange range)
|
||
{
|
||
switch (property.propertyType)
|
||
{
|
||
case SerializedPropertyType.Float:
|
||
property.floatValue = UnityEngine.Random.Range(range.MinFloat, range.MaxFloat);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Integer:
|
||
property.intValue = UnityEngine.Random.Range(range.MinInt, range.MaxInt + 1);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Boolean:
|
||
property.boolValue = UnityEngine.Random.value < range.BoolProbability;
|
||
return true;
|
||
|
||
case SerializedPropertyType.Vector2:
|
||
property.vector2Value = new Vector2(
|
||
UnityEngine.Random.Range(range.MinVector.x, range.MaxVector.x),
|
||
UnityEngine.Random.Range(range.MinVector.y, range.MaxVector.y)
|
||
);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Vector3:
|
||
property.vector3Value = new Vector3(
|
||
UnityEngine.Random.Range(range.MinVector.x, range.MaxVector.x),
|
||
UnityEngine.Random.Range(range.MinVector.y, range.MaxVector.y),
|
||
UnityEngine.Random.Range(range.MinVector.z, range.MaxVector.z)
|
||
);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Vector4:
|
||
property.vector4Value = new Vector4(
|
||
UnityEngine.Random.Range(range.MinVector.x, range.MaxVector.x),
|
||
UnityEngine.Random.Range(range.MinVector.y, range.MaxVector.y),
|
||
UnityEngine.Random.Range(range.MinVector.z, range.MaxVector.z),
|
||
UnityEngine.Random.Range(0f, 1f) // w component
|
||
);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Quaternion:
|
||
if (range.RotationMode == RotationMode.ConstrainedEuler)
|
||
{
|
||
// Use constrained euler angles
|
||
property.quaternionValue = Quaternion.Euler(
|
||
UnityEngine.Random.Range(range.MinRotation.x, range.MaxRotation.x),
|
||
UnityEngine.Random.Range(range.MinRotation.y, range.MaxRotation.y),
|
||
UnityEngine.Random.Range(range.MinRotation.z, range.MaxRotation.z)
|
||
);
|
||
}
|
||
else
|
||
{
|
||
// Generate a truly random rotation using a uniform distribution
|
||
// This uses a technique that avoids the gimbal lock issues with Euler angles
|
||
float u1 = UnityEngine.Random.value;
|
||
float u2 = UnityEngine.Random.value;
|
||
float u3 = UnityEngine.Random.value;
|
||
|
||
// Convert uniform random values to a uniformly distributed rotation
|
||
float sqrt1MinusU1 = Mathf.Sqrt(1 - u1);
|
||
float sqrtU1 = Mathf.Sqrt(u1);
|
||
|
||
property.quaternionValue = new Quaternion(
|
||
sqrt1MinusU1 * Mathf.Sin(2 * Mathf.PI * u2),
|
||
sqrt1MinusU1 * Mathf.Cos(2 * Mathf.PI * u2),
|
||
sqrtU1 * Mathf.Sin(2 * Mathf.PI * u3),
|
||
sqrtU1 * Mathf.Cos(2 * Mathf.PI * u3)
|
||
);
|
||
}
|
||
return true;
|
||
|
||
case SerializedPropertyType.Color:
|
||
property.colorValue = new Color(
|
||
Mathf.Lerp(range.MinColor.r, range.MaxColor.r, UnityEngine.Random.value),
|
||
Mathf.Lerp(range.MinColor.g, range.MaxColor.g, UnityEngine.Random.value),
|
||
Mathf.Lerp(range.MinColor.b, range.MaxColor.b, UnityEngine.Random.value),
|
||
Mathf.Lerp(range.MinColor.a, range.MaxColor.a, UnityEngine.Random.value)
|
||
);
|
||
return true;
|
||
|
||
case SerializedPropertyType.Enum:
|
||
int enumValueCount = property.enumNames.Length;
|
||
if (enumValueCount > 0)
|
||
{
|
||
property.enumValueIndex = UnityEngine.Random.Range(0, enumValueCount);
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Helper class and enum for property range management
|
||
private enum PropertyType
|
||
{
|
||
Float,
|
||
Int,
|
||
Bool,
|
||
Vector2,
|
||
Vector3,
|
||
Vector4,
|
||
Color,
|
||
Quaternion,
|
||
Enum,
|
||
Unsupported
|
||
}
|
||
|
||
private enum RotationMode
|
||
{
|
||
FullRandom,
|
||
ConstrainedEuler
|
||
}
|
||
|
||
private class PropertyRange
|
||
{
|
||
public PropertyType Type;
|
||
|
||
// For Float
|
||
public float MinFloat;
|
||
public float MaxFloat;
|
||
|
||
// For Int
|
||
public int MinInt;
|
||
public int MaxInt;
|
||
|
||
// For Vector types
|
||
public Vector3 MinVector;
|
||
public Vector3 MaxVector;
|
||
|
||
// For Color
|
||
public Color MinColor;
|
||
public Color MaxColor;
|
||
|
||
// For Bool
|
||
public float BoolProbability;
|
||
|
||
// For Quaternion
|
||
public RotationMode RotationMode = RotationMode.FullRandom;
|
||
public Vector3 MinRotation = Vector3.zero;
|
||
public Vector3 MaxRotation = new Vector3(360, 360, 360);
|
||
}
|
||
}
|