Files
AppleHillsProduction/Assets/Editor/BatchRandomizerWindow.cs
2025-10-06 16:57:47 +02:00

1154 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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