Files
AppleHillsProduction/Assets/Editor/Tools/BatchRandomizerWindow.cs

1154 lines
43 KiB
C#
Raw Normal View History

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