diff --git a/Assets/Editor/BatchRandomizerWindow.cs b/Assets/Editor/BatchRandomizerWindow.cs new file mode 100644 index 00000000..5387cb82 --- /dev/null +++ b/Assets/Editor/BatchRandomizerWindow.cs @@ -0,0 +1,1153 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System; + +public class BatchRandomizerWindow : EditorWindow +{ + private List selectedObjects = new List(); + private List serializedObjects = new List(); + private Dictionary> objectComponents = new Dictionary>(); + private bool includeChildren = true; + private Vector2 propertiesScrollPosition; + private Vector2 selectedPropertiesScrollPosition; + private Dictionary propertyFoldouts = new Dictionary(); + private Dictionary componentFoldouts = new Dictionary(); + private Dictionary propertyRanges = new Dictionary(); + private Dictionary selectedProperties = new Dictionary(); + 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 propertyOccurrences = new Dictionary(); + private HashSet allPropertyPaths = new HashSet(); + private List selectedPropertyList = new List(); + + // Used to track expanded paths in the UI + private HashSet expandedPaths = new HashSet(); + + // Component-specific properties + private Dictionary> componentsByType = new Dictionary>(); + + [MenuItem("Tools/Batch Property Randomizer")] + public static void ShowWindow() + { + GetWindow("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 previouslySelectedProperties = new HashSet(); + 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(); + CollectComponentsForObject(obj); + + if (includeChildren) + { + // Add all children + Transform[] childTransforms = obj.GetComponentsInChildren(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(); + 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(); + 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(); + } + + 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 toRemove = new List(); + 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 propertiesToRemove = new List(); + + 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 propertyItems = new List(); + + // Collect all properties from all component types + foreach (var typeEntry in componentsByType.OrderBy(entry => entry.Key.Name)) + { + Type componentType = typeEntry.Key; + List 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 displayedProps = new HashSet(); + + 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 propertyItems = new List(); + + // Group properties by component for organization + Dictionary> componentProperties = GroupPropertiesByComponent(); + + foreach (var componentEntry in componentProperties) + { + string componentName = componentEntry.Key; + List 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> GroupPropertiesByComponent() + { + Dictionary> result = new Dictionary>(); + + 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(); + + 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 objectsToRecord = new List(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); + } +} diff --git a/Assets/Editor/BatchRandomizerWindow.cs.meta b/Assets/Editor/BatchRandomizerWindow.cs.meta new file mode 100644 index 00000000..4ce740dc --- /dev/null +++ b/Assets/Editor/BatchRandomizerWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b3a76a11aae4feba5a7bf8ccfd11d38 +timeCreated: 1759760984 \ No newline at end of file diff --git a/Assets/Prefabs/Items/PrefabsPLACEHOLDER/HeadBand.prefab b/Assets/Prefabs/Items/PrefabsPLACEHOLDER/HeadBand.prefab index 8ed1fc97..8a0981ce 100644 --- a/Assets/Prefabs/Items/PrefabsPLACEHOLDER/HeadBand.prefab +++ b/Assets/Prefabs/Items/PrefabsPLACEHOLDER/HeadBand.prefab @@ -150,5 +150,5 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: stepData: {fileID: 11400000, guid: 5700dd3bf16fa9e4aa9905379118d1bd, type: 2} - indicatorPrefab: {fileID: 0} + puzzleIndicator: {fileID: 0} drawPromptRangeGizmo: 1