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); } }