// Unity C# reference source // Copyright (c) Unity Technologies. For terms of use, see // https://unity3d.com/legal/licenses/Unity_Reference_Only_License using UnityEngine; using System.Collections.Generic; using UnityEditorInternal; using UnityEditor.EditorTools; namespace UnityEditor { class CollisionModuleUI : ModuleUI { enum CollisionTypes { Plane = 0, World = 1 } enum CollisionModes { Mode3D = 0, Mode2D = 1 } enum PlaneVizType { Grid, Solid } SerializedProperty m_Type; SerializedProperty m_Planes; SerializedMinMaxCurve m_Dampen; SerializedMinMaxCurve m_Bounce; SerializedMinMaxCurve m_LifetimeLossOnCollision; SerializedProperty m_MinKillSpeed; SerializedProperty m_MaxKillSpeed; SerializedProperty m_RadiusScale; SerializedProperty m_CollidesWith; SerializedProperty m_CollidesWithDynamic; SerializedProperty m_MaxCollisionShapes; SerializedProperty m_Quality; SerializedProperty m_VoxelSize; SerializedProperty m_CollisionMessages; SerializedProperty m_CollisionMode; SerializedProperty m_ColliderForce; SerializedProperty m_MultiplyColliderForceByCollisionAngle; SerializedProperty m_MultiplyColliderForceByParticleSpeed; SerializedProperty m_MultiplyColliderForceByParticleSize; ReorderableList m_PlanesList; static PlaneVizType m_PlaneVisualizationType = PlaneVizType.Solid; static float m_ScaleGrid = 1.0f; static bool s_VisualizeBounds = false; internal static PrefColor s_CollisionBoundsColor = new PrefColor("Particle System/Collision Bounds", 0.0f, 1.0f, 0.0f, 1.0f); static readonly string s_UndoCollisionPlaneString = L10n.Tr("Modified Collision Plane Transform"); class Texts { public GUIContent lifetimeLoss = EditorGUIUtility.TrTextContent("Lifetime Loss", "When particle collides, it will lose this fraction of its Start Lifetime"); public GUIContent planes = EditorGUIUtility.TrTextContent("Planes", "Planes are defined by assigning a reference to a transform. This transform can be any transform in the scene and can be animated. Multiple planes can be used. Note: the Y-axis is used as the plane normal."); public GUIContent createPlane = EditorGUIUtility.TrTextContent("", "Create an empty GameObject and assign it as a plane."); public GUIContent minKillSpeed = EditorGUIUtility.TrTextContent("Min Kill Speed", "When particles collide and their speed is lower than this value, they are killed."); public GUIContent maxKillSpeed = EditorGUIUtility.TrTextContent("Max Kill Speed", "When particles collide and their speed is higher than this value, they are killed."); public GUIContent dampen = EditorGUIUtility.TrTextContent("Dampen", "When particle collides, it will lose this fraction of its speed. Unless this is set to 0.0, particle will become slower after collision."); public GUIContent bounce = EditorGUIUtility.TrTextContent("Bounce", "When particle collides, the bounce is scaled with this value. The bounce is the upwards motion in the plane normal direction."); public GUIContent radiusScale = EditorGUIUtility.TrTextContent("Radius Scale", "Scale particle bounds by this amount to get more precise collisions."); public GUIContent visualization = EditorGUIUtility.TrTextContent("Visualization", "Only used for visualizing the planes: Wireframe or Solid."); public GUIContent scalePlane = EditorGUIUtility.TrTextContent("Scale Plane", "Resizes the visualization planes."); public GUIContent visualizeBounds = EditorGUIUtility.TrTextContent("Visualize Bounds", "Render the collision bounds of the particles."); public GUIContent collidesWith = EditorGUIUtility.TrTextContent("Collides With", "Collides the particles with colliders included in the layermask."); public GUIContent collidesWithDynamic = EditorGUIUtility.TrTextContent("Enable Dynamic Colliders", "Should particles collide with dynamic objects?"); public GUIContent maxCollisionShapes = EditorGUIUtility.TrTextContent("Max Collision Shapes", "How many collision shapes can be considered for particle collisions. Excess shapes will be ignored. Terrains take priority."); public GUIContent quality = EditorGUIUtility.TrTextContent("Collision Quality", "Quality of world collisions. Medium and low quality are approximate and may leak particles."); public GUIContent voxelSize = EditorGUIUtility.TrTextContent("Voxel Size", "Size of voxels in the collision cache. Smaller values improve accuracy but require higher memory usage and are less efficient."); public GUIContent collisionMessages = EditorGUIUtility.TrTextContent("Send Collision Messages", "Send collision callback messages."); public GUIContent collisionType = EditorGUIUtility.TrTextContent("Type", "Collide with a list of Planes, or the Physics World."); public GUIContent collisionMode = EditorGUIUtility.TrTextContent("Mode", "Use 3D Physics or 2D Physics."); public GUIContent colliderForce = EditorGUIUtility.TrTextContent("Collider Force", "Control the strength of particle forces on colliders."); public GUIContent multiplyColliderForceByCollisionAngle = EditorGUIUtility.TrTextContent("Multiply by Collision Angle", "Should the force be proportional to the angle of the particle collision? A particle collision directly along the collision normal produces all the specified force whilst collisions away from the collision normal produce less force."); public GUIContent multiplyColliderForceByParticleSpeed = EditorGUIUtility.TrTextContent("Multiply by Particle Speed", "Should the force be proportional to the particle speed?"); public GUIContent multiplyColliderForceByParticleSize = EditorGUIUtility.TrTextContent("Multiply by Particle Size", "Should the force be proportional to the particle size?"); public GUIContent sceneTools = EditorGUIUtility.TrTextContent("Scene Tools"); public GUIContent[] collisionTypes = new GUIContent[] { EditorGUIUtility.TrTextContent("Planes"), EditorGUIUtility.TrTextContent("World") }; public GUIContent[] collisionModes = new GUIContent[] { EditorGUIUtility.TrTextContent("3D"), EditorGUIUtility.TrTextContent("2D") }; public GUIContent[] qualitySettings = new GUIContent[] { EditorGUIUtility.TrTextContent("High"), EditorGUIUtility.TrTextContent("Medium (Static Colliders)"), EditorGUIUtility.TrTextContent("Low (Static Colliders)") }; public GUIContent[] planeVizTypes = new GUIContent[] { EditorGUIUtility.TrTextContent("Grid"), EditorGUIUtility.TrTextContent("Solid") }; } private static Texts s_Texts; [EditorTool("Transform Collider", typeof(ParticleSystem))] class CollisionModuleTransformTool : EditorTool { private HashSet m_ScenePlanes = new HashSet(); private static Transform s_SelectedTransform; // static to ensure there is only one selected Transform across multiple particle systems public override GUIContent toolbarIcon { get { return EditorGUIUtility.TrIconContent("TransformTool", "Collision module plane editing mode."); } } public override bool IsAvailable() { foreach (var t in targets) { var ps = t as ParticleSystem; if (ps == null) continue; var collision = ps.collision; if (collision.enabled && collision.type == ParticleSystemCollisionType.Planes) return true; } return false; } public override void OnToolGUI(EditorWindow window) { var firstTransform = SyncScenePlanes(); if (m_ScenePlanes.Count == 0) return; // Clear invalid selection (ie when selecting other system) if (s_SelectedTransform != null) { if (!m_ScenePlanes.Contains(s_SelectedTransform)) s_SelectedTransform = null; } // Always try to select first collider if no selection exists if (s_SelectedTransform == null) s_SelectedTransform = firstTransform; Event evt = Event.current; Color origCol = Handles.color; Color col = new Color(1, 1, 1, 0.5F); bool isPlaying = EditorApplication.isPlaying; foreach (var transform in m_ScenePlanes) { Vector3 position = transform.position; Quaternion rotation = transform.rotation; Vector3 right = rotation * Vector3.right; Vector3 up = rotation * Vector3.up; Vector3 forward = rotation * Vector3.forward; bool isPlayingAndStatic = isPlaying && transform.gameObject.isStatic; if (ReferenceEquals(s_SelectedTransform, transform)) { EditorGUI.BeginChangeCheck(); var newPosition = transform.position; var newRotation = transform.rotation; using (new EditorGUI.DisabledScope(isPlayingAndStatic)) { if (isPlayingAndStatic) Handles.ShowSceneViewLabel(position, Handles.s_StaticLabel); Handles.TransformHandle(ref newPosition, ref newRotation); } if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(transform, s_UndoCollisionPlaneString); transform.position = newPosition; transform.rotation = newRotation; ParticleSystemEditorUtils.PerformCompleteResimulation(); } } else { float handleSize = HandleUtility.GetHandleSize(position) * 0.6f; EventType oldEventType = evt.type; // we want ignored mouse up events to check for dragging off of scene view if (evt.type == EventType.Ignore && evt.rawType == EventType.MouseUp) oldEventType = evt.rawType; Handles.FreeMoveHandle(position, handleSize, Vector3.zero, Handles.RectangleHandleCap); // Detect selected plane (similar to TreeEditor) if (oldEventType == EventType.MouseDown && evt.type == EventType.Used) { s_SelectedTransform = transform; oldEventType = EventType.Used; GUIUtility.hotControl = 0; // Reset hot control or the FreeMoveHandle will prevent input to the new Handles. (case 873514) } } Handles.color = col; Color color = Handles.s_ColliderHandleColor * 0.9f; if (isPlayingAndStatic) color.a *= 0.2f; if (m_PlaneVisualizationType == PlaneVizType.Grid) { DrawGrid(position, right, forward, up, color); } else { DrawSolidPlane(position, rotation, color, Color.yellow); } } Handles.color = origCol; } private Transform SyncScenePlanes() { m_ScenePlanes.Clear(); Transform firstTransform = null; foreach (var t in targets) { var ps = t as ParticleSystem; if (ps == null) continue; if (ps.collision.type != ParticleSystemCollisionType.Planes) continue; for (int planeIndex = 0; planeIndex < ps.collision.planeCount; planeIndex++) { var transform = ps.collision.GetPlane(planeIndex); if (transform != null) { m_ScenePlanes.Add(transform); if (firstTransform == null) firstTransform = transform; } } } return firstTransform; } } public CollisionModuleUI(ParticleSystemUI owner, SerializedObject o, string displayName) : base(owner, o, "CollisionModule", displayName) { m_ToolTip = "Allows you to specify multiple collision planes that the particle can collide with."; } private bool editingPlanes { get { return EditorToolManager.activeTool is CollisionModuleTransformTool; } } protected override void Init() { // Already initialized? if (m_Type != null) return; if (s_Texts == null) s_Texts = new Texts(); m_Type = GetProperty("type"); m_Planes = GetProperty("m_Planes"); m_Dampen = new SerializedMinMaxCurve(this, s_Texts.dampen, "m_Dampen"); m_Dampen.m_AllowCurves = false; m_Bounce = new SerializedMinMaxCurve(this, s_Texts.bounce, "m_Bounce"); m_Bounce.m_AllowCurves = false; m_LifetimeLossOnCollision = new SerializedMinMaxCurve(this, s_Texts.lifetimeLoss, "m_EnergyLossOnCollision"); m_LifetimeLossOnCollision.m_AllowCurves = false; m_MinKillSpeed = GetProperty("minKillSpeed"); m_MaxKillSpeed = GetProperty("maxKillSpeed"); m_RadiusScale = GetProperty("radiusScale"); m_PlaneVisualizationType = (PlaneVizType)EditorPrefs.GetInt("PlaneColisionVizType", (int)PlaneVizType.Solid); m_ScaleGrid = EditorPrefs.GetFloat("ScalePlaneColision", 1f); s_VisualizeBounds = EditorPrefs.GetBool("VisualizeBounds", false); m_CollidesWith = GetProperty("collidesWith"); m_CollidesWithDynamic = GetProperty("collidesWithDynamic"); m_MaxCollisionShapes = GetProperty("maxCollisionShapes"); m_Quality = GetProperty("quality"); m_VoxelSize = GetProperty("voxelSize"); m_CollisionMessages = GetProperty("collisionMessages"); m_CollisionMode = GetProperty("collisionMode"); m_ColliderForce = GetProperty("colliderForce"); m_MultiplyColliderForceByCollisionAngle = GetProperty("multiplyColliderForceByCollisionAngle"); m_MultiplyColliderForceByParticleSpeed = GetProperty("multiplyColliderForceByParticleSpeed"); m_MultiplyColliderForceByParticleSize = GetProperty("multiplyColliderForceByParticleSize"); m_PlanesList = new ReorderableList(m_Planes.m_SerializedObject, m_Planes, true, false, true, true); m_PlanesList.headerHeight = 0; m_PlanesList.drawElementCallback = DrawPlaneElementCallback; m_PlanesList.elementHeight = kReorderableListElementHeight; m_PlanesList.onAddCallback = OnAddPlaneElementCallback; } override public void OnInspectorGUI(InitialModuleUI initial) { EditorGUI.BeginChangeCheck(); CollisionTypes type = (CollisionTypes)GUIPopup(s_Texts.collisionType, m_Type, s_Texts.collisionTypes); if (EditorGUI.EndChangeCheck()) ToolManager.RefreshAvailableTools(); if (type == CollisionTypes.Plane) DoListOfPlanesGUI(); else GUIPopup(s_Texts.collisionMode, m_CollisionMode, s_Texts.collisionModes); GUIMinMaxCurve(s_Texts.dampen, m_Dampen); GUIMinMaxCurve(s_Texts.bounce, m_Bounce); GUIMinMaxCurve(s_Texts.lifetimeLoss, m_LifetimeLossOnCollision); GUIFloat(s_Texts.minKillSpeed, m_MinKillSpeed); GUIFloat(s_Texts.maxKillSpeed, m_MaxKillSpeed); GUIFloat(s_Texts.radiusScale, m_RadiusScale); if (type == CollisionTypes.World) { GUIPopup(s_Texts.quality, m_Quality, s_Texts.qualitySettings); EditorGUI.indentLevel++; GUILayerMask(s_Texts.collidesWith, m_CollidesWith); GUIInt(s_Texts.maxCollisionShapes, m_MaxCollisionShapes); if (m_Quality.intValue == 0) GUIToggle(s_Texts.collidesWithDynamic, m_CollidesWithDynamic); else GUIFloat(s_Texts.voxelSize, m_VoxelSize); EditorGUI.indentLevel--; GUIFloat(s_Texts.colliderForce, m_ColliderForce); EditorGUI.indentLevel++; GUIToggle(s_Texts.multiplyColliderForceByCollisionAngle, m_MultiplyColliderForceByCollisionAngle); GUIToggle(s_Texts.multiplyColliderForceByParticleSpeed, m_MultiplyColliderForceByParticleSpeed); GUIToggle(s_Texts.multiplyColliderForceByParticleSize, m_MultiplyColliderForceByParticleSize); EditorGUI.indentLevel--; } GUIToggle(s_Texts.collisionMessages, m_CollisionMessages); if (EditorGUIUtility.comparisonViewMode == EditorGUIUtility.ComparisonViewMode.None) { EditorGUILayout.BeginVertical("GroupBox"); if (type == CollisionTypes.Plane) { var editorTools = new List(2); EditorToolManager.GetComponentTools(x => x.GetEditor() is CollisionModuleTransformTool, editorTools, true); GUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel(s_Texts.sceneTools, ParticleSystemStyles.Get().label, ParticleSystemStyles.Get().label); EditorGUILayout.EditorToolbar(editorTools); GUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUI.BeginChangeCheck(); m_PlaneVisualizationType = (PlaneVizType)GUIPopup(s_Texts.visualization, (int)m_PlaneVisualizationType, s_Texts.planeVizTypes); if (EditorGUI.EndChangeCheck()) { EditorPrefs.SetInt("PlaneColisionVizType", (int)m_PlaneVisualizationType); } EditorGUI.BeginChangeCheck(); m_ScaleGrid = GUIFloat(s_Texts.scalePlane, m_ScaleGrid, "f2"); if (EditorGUI.EndChangeCheck()) { m_ScaleGrid = Mathf.Max(0f, m_ScaleGrid); EditorPrefs.SetFloat("ScalePlaneColision", m_ScaleGrid); } } else { GUILayout.Label(s_Texts.sceneTools, ParticleSystemStyles.Get().label); } EditorGUI.BeginChangeCheck(); s_VisualizeBounds = GUIToggle(s_Texts.visualizeBounds, s_VisualizeBounds); if (EditorGUI.EndChangeCheck()) EditorPrefs.SetBool("VisualizeBounds", s_VisualizeBounds); EditorGUILayout.EndVertical(); } } private static GameObject CreateEmptyGameObject(string name, ParticleSystem parentOfGameObject) { GameObject go = new GameObject(name); if (go) { if (parentOfGameObject) go.transform.parent = parentOfGameObject.transform; Undo.RegisterCreatedObjectUndo(go, "Created `" + name + "`"); return go; } return null; } private void DoListOfPlanesGUI() { // only allow editing in single edit mode if (m_ParticleSystemUI.multiEdit) { EditorGUILayout.HelpBox("Trigger editing is only available when editing a single Particle System", MessageType.Info, true); return; } m_PlanesList.DoLayoutList(); } void OnAddPlaneElementCallback(ReorderableList list) { int index = m_Planes.arraySize; m_Planes.InsertArrayElementAtIndex(index); m_Planes.GetArrayElementAtIndex(index).objectReferenceValue = null; } void DrawPlaneElementCallback(Rect rect, int index, bool isActive, bool isFocused) { rect.height = kSingleLineHeight; var plane = m_Planes.GetArrayElementAtIndex(index); Rect objectRect = new Rect(rect.x, rect.y, rect.width - EditorGUI.kSpacing - ParticleSystemStyles.Get().plus.fixedWidth, rect.height); GUIObject(objectRect, GUIContent.none, plane, null); if (plane.objectReferenceValue == null) { Rect buttonRect = new Rect(objectRect.xMax + EditorGUI.kSpacing, rect.y + 4, ParticleSystemStyles.Get().plus.fixedWidth, rect.height); if (GUI.Button(buttonRect, s_Texts.createPlane, ParticleSystemStyles.Get().plus)) { GameObject go = CreateEmptyGameObject("Plane Transform " + (index + 1), m_ParticleSystemUI.m_ParticleSystems[0]); go.transform.localPosition = new Vector3(0, 0, 10 + index); // ensure each plane is not at same pos go.transform.localEulerAngles = (new Vector3(-90, 0, 0)); // make the plane normal point towards the forward axis of the particle system plane.objectReferenceValue = go.GetComponent(); } } } override public void OnSceneViewGUI() { RenderCollisionBounds(); } void RenderCollisionBounds() { if (s_VisualizeBounds == false) return; Color oldColor = Handles.color; Handles.color = s_CollisionBoundsColor; Matrix4x4 oldMatrix = Handles.matrix; Vector3[] points0 = new Vector3[20]; Vector3[] points1 = new Vector3[20]; Vector3[] points2 = new Vector3[20]; Handles.SetDiscSectionPoints(points0, Vector3.zero, Vector3.forward, Vector3.right, 360, 1.0f); Handles.SetDiscSectionPoints(points1, Vector3.zero, Vector3.up, -Vector3.right, 360, 1.0f); Handles.SetDiscSectionPoints(points2, Vector3.zero, Vector3.right, Vector3.up, 360, 1.0f); Vector3[] points = new Vector3[points0.Length + points1.Length + points2.Length]; points0.CopyTo(points, 0); points1.CopyTo(points, 20); points2.CopyTo(points, 40); foreach (ParticleSystem ps in m_ParticleSystemUI.m_ParticleSystems) { if (!ps.collision.enabled) continue; ParticleSystem.Particle[] particles = new ParticleSystem.Particle[ps.particleCount]; int count = ps.GetParticles(particles); Matrix4x4 transform = Matrix4x4.identity; if (ps.main.simulationSpace == ParticleSystemSimulationSpace.Local) { transform = ps.localToWorldMatrix; } for (int i = 0; i < count; i++) { ParticleSystem.Particle particle = particles[i]; Vector3 size = particle.GetCurrentSize3D(ps); float radius = System.Math.Max(size.x, System.Math.Max(size.y, size.z)) * 0.5f * ps.collision.radiusScale; Handles.matrix = transform * Matrix4x4.TRS(particle.position, Quaternion.identity, new Vector3(radius, radius, radius)); Handles.DrawPolyLine(points); } } Handles.color = oldColor; Handles.matrix = oldMatrix; } private static void DrawSolidPlane(Vector3 pos, Quaternion rot, Color faceColor, Color edgeColor) { if (Event.current.type != EventType.Repaint) return; var oldMatrix = Handles.matrix; float scale = 10 * m_ScaleGrid; Handles.matrix = Matrix4x4.TRS(pos, rot, new Vector3(scale, scale, scale)) * Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(90, 0, 0), Vector3.one); // Rotate plane by 90 Handles.DrawSolidRectangleWithOutline(new Rect(-0.5f, -0.5f, 1, 1), faceColor, edgeColor); Handles.DrawLine(Vector3.zero, Vector3.back / scale); Handles.matrix = oldMatrix; } private static void DrawGrid(Vector3 pos, Vector3 axis1, Vector3 axis2, Vector3 normal, Color color) { if (Event.current.type != EventType.Repaint) return; HandleUtility.ApplyWireMaterial(); if (color.a > 0) { GL.Begin(GL.LINES); float lineLength = 10f; int numLines = 11; lineLength *= m_ScaleGrid; numLines = (int)lineLength; numLines = Mathf.Clamp(numLines, 10, 40); if (numLines % 2 == 0) numLines++; float halfLength = lineLength * 0.5f; float distBetweenLines = lineLength / (numLines - 1); Vector3 v1 = axis1 * lineLength; Vector3 v2 = axis2 * lineLength; Vector3 dist1 = axis1 * distBetweenLines; Vector3 dist2 = axis2 * distBetweenLines; Vector3 startPos = pos - axis1 * halfLength - axis2 * halfLength; for (int i = 0; i < numLines; i++) { if (i % 2 == 0) GL.Color(color * 0.7f); else GL.Color(color); // Axis1 GL.Vertex(startPos + i * dist1); GL.Vertex(startPos + i * dist1 + v2); // Axis2 GL.Vertex(startPos + i * dist2); GL.Vertex(startPos + i * dist2 + v1); } GL.Color(color); GL.Vertex(pos); GL.Vertex(pos + normal); GL.End(); } } override public void UpdateCullingSupportedString(ref string text) { text += "\nCollision module is enabled."; } } } // namespace UnityEditor