#if UNITY_EDITOR using MonKey.Extensions; using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace MonKey.Editor.Commands { public static class ConstrainUtilities { [Command("Look At", QuickName = "LI", Help = "Constrains the selected objects to always look " + "at another one (by default, looks at the mouse)" , Category = "Transform")] public static void LookAtConstrain() { MonkeyEditorUtils.AddSceneCommand( new LookAtSceneCommand(null, Selection.gameObjects, new Vector3(0, 1, 0), new Axis[0])); } [Command("Look At 2D", QuickName = "LI2", Help = "Constrains the selected objects to always look " + "at another one in 2D space(by default the first selected)" , DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, Category = "2D/Transform")] public static void LookAtConstrain2D() { MonkeyEditorUtils.AddSceneCommand( new LookAt2DSceneCommand(null, Selection.gameObjects)); } [Command("Look At Mouse", QuickName = "LM", Help = "Makes the selected objects look at the mouse until confirmed" , DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, AlwaysShow = true, Category = "Transform")] public static void LookAtMouseConstrain() { MonkeyEditorUtils.AddSceneCommand( new LookAtSceneConfirmedCommand(null, Selection.gameObjects, Vector3.up, new Axis[0])); } [Command("Look At Mouse 2D", QuickName = "LM2", Help = "Makes the selected objects look at the mouse until confirmed" , DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, AlwaysShow = true , Category = "2D/Transform")] public static void LookAtMouseConstrain2D() { MonkeyEditorUtils.AddSceneCommand( new LookAt2DSceneConfirmedCommand(null, Selection.gameObjects)); } public static Axis[] DefaultLockedAxis() { return new[] { Axis.NONE }; } public static GameObject FirstSelected() { return MonkeyEditorUtils.OrderedSelectedGameObjects.ElementAt(0); } public static GameObject SecondSelected() { return MonkeyEditorUtils.OrderedSelectedGameObjects.ElementAt(1); } public static Vector3 ZeroVector() { return Vector3.zero; } public class LookAtSceneCommand : TimedSceneCommand { private readonly LookAtLogic logic; public LookAtSceneCommand(GameObject toLookAt, GameObject[] constrained, Vector3 worldUpVector, Axis[] lockedAxes) : base(0) { SceneCommandName = "Constrain: Look At " + (toLookAt ? toLookAt.name : "Mouse"); logic = new LookAtLogic(toLookAt, constrained, worldUpVector, lockedAxes); } public override void OnSceneGUI() { base.OnSceneGUI(); logic.OnSceneGUI(); } public override void Update() { base.Update(); logic.Update(); } public override void Stop() { base.Stop(); logic.Stop(); } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("Object To Look At", ref logic.ToLookAt); if (!logic.ToLookAt) DisplayMessage("Currently Looking At The Mouse Position"); DisplayBoolOption("Reverse Direction", ref logic.ReverseDirection); DisplayObjectListOption("Objects Looking", logic.Constrained); DisplayVectorOption("World Up Vector", ref logic.WorldUpVector); DisplayBoolOption("Show Debug Text", ref logic.ShowDebugText); } } public class LookAt2DSceneCommand : TimedSceneCommand { private readonly LookAt2DLogic logic; public LookAt2DSceneCommand(GameObject toLookAt, GameObject[] constrained) : base(0) { SceneCommandName = "Constrain: Look At " + (toLookAt ? toLookAt.name : "Mouse"); logic = new LookAt2DLogic(toLookAt, constrained); } public override void OnSceneGUI() { base.OnSceneGUI(); logic.OnSceneGUI(); } public override void Update() { base.Update(); logic.Update(); } public override void Stop() { base.Stop(); logic.Stop(); } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("Object To Look At", ref logic.ToLookAt); DisplayBoolOption("Show Debug Text", ref logic.ShowDebugText); DisplayObjectListOption("Objects Looking", logic.Constrained); } } public class LookAtSceneConfirmedCommand : ConfirmedCommand { private readonly LookAtLogic logic; public LookAtSceneConfirmedCommand(GameObject toLookAt, GameObject[] constrained, Vector3 worldUpVector, Axis[] lockedAxes) { SceneCommandName = "Constrain: Look At " + (toLookAt ? toLookAt.name : "Mouse"); logic = new LookAtLogic(toLookAt, constrained, worldUpVector, lockedAxes); } public override void OnSceneGUI() { base.OnSceneGUI(); logic.OnSceneGUI(); } public override void Update() { base.Update(); logic.Update(); } public override void Stop() { base.Stop(); logic.Stop(); } } public class LookAt2DSceneConfirmedCommand : ConfirmedCommand { private readonly LookAt2DLogic logic; public LookAt2DSceneConfirmedCommand(GameObject toLookAt, GameObject[] constrained) { SceneCommandName = "Constrain: Look At " + (toLookAt ? toLookAt.name : "Mouse"); logic = new LookAt2DLogic(toLookAt, constrained); } public override void OnSceneGUI() { base.OnSceneGUI(); logic.OnSceneGUI(); } public override void Update() { base.Update(); logic.Update(); } public override void Stop() { base.Stop(); logic.Stop(); } } public class LookAtLogic { public GameObject ToLookAt; public List Constrained; public Vector3 WorldUpVector; public bool ShowDebugText = true; public bool ReverseDirection = false; private readonly Quaternion[] initialRotations; private readonly Axis[] lockedAxes; public LookAtLogic(GameObject toLookAt, GameObject[] constrained, Vector3 worldUpVector, Axis[] lockedAxes) { this.ToLookAt = toLookAt; this.WorldUpVector = worldUpVector; this.Constrained = constrained.Where(_ => _ != toLookAt).ToList(); this.lockedAxes = lockedAxes; initialRotations = this.Constrained.Convert(_ => _.transform.rotation).ToArray(); Selection.objects = new Object[] { toLookAt }; } public void OnSceneGUI() { if (!ShowDebugText) return; Constrained.RemoveAll(_ => !_); foreach (GameObject o in Constrained) { Handles.Label(o.transform.position, "Lock Look At '" + ToLookAt + "'"); } } public void Update() { int i = 0; Constrained.RemoveAll(_ => !_); foreach (var gameObject in Constrained) { if (ToLookAt != null) { if (WorldUpVector == Vector3.zero) gameObject.transform.LookAt(ToLookAt.transform); else { gameObject.transform.LookAt(ToLookAt.transform, WorldUpVector); } } else { bool collision; Vector3 normal; Vector3 mouseRaycastPosition = MonkeyEditorUtils.GetMouseRayCastedPosition(new Transform[0], 0, out collision, out normal); if (WorldUpVector == Vector3.zero) gameObject.transform.LookAt(mouseRaycastPosition); else { gameObject.transform.LookAt(mouseRaycastPosition, WorldUpVector); } } if (ReverseDirection) { gameObject.transform.Rotate(gameObject.transform.up, 180); } if (lockedAxes.Length > 0) { Vector3 euler = gameObject.transform.rotation.eulerAngles; Vector3 initialEuler = initialRotations[i].eulerAngles; gameObject.transform.rotation = Quaternion.Euler( lockedAxes.Contains(Axis.X) ? initialEuler.x : euler.x, lockedAxes.Contains(Axis.Y) ? initialEuler.y : euler.y, lockedAxes.Contains(Axis.Z) ? initialEuler.z : euler.z); } i++; } } public void Stop() { Quaternion[] newRotations = Constrained.Convert(_ => _.transform.rotation).ToArray(); Constrained.RemoveAll(_ => !_); for (int i = 0; i < Constrained.Count; i++) { Constrained[i].transform.rotation = initialRotations[i]; } int id = MonkeyEditorUtils.CreateUndoGroup("Look At Constrain"); for (int i = 0; i < Constrained.Count; i++) { Undo.RecordObject(Constrained[i].transform, "Look At Rotation"); Constrained[i].transform.rotation = newRotations[i]; } Undo.CollapseUndoOperations(id); } } public class LookAt2DLogic { public GameObject ToLookAt; public List Constrained; public bool ShowDebugText = true; private readonly Quaternion[] initialRotations; public LookAt2DLogic(GameObject toLookAt, GameObject[] constrained) { this.ToLookAt = toLookAt; this.Constrained = new List(constrained.Where(_ => _ != toLookAt)); initialRotations = this.Constrained.Convert(_ => _.transform.rotation).ToArray(); Selection.objects = new Object[] { toLookAt }; } public void OnSceneGUI() { if (!ShowDebugText) return; Constrained.RemoveAll(_ => !_); foreach (GameObject o in Constrained) { Handles.Label(o.transform.position, "Lock Look At '" + ToLookAt + "'"); } } public void Update() { Constrained.RemoveAll(_ => !_); Vector2 mousePos = HandleUtility.GUIPointToScreenPixelCoordinate(MonkeyEditorUtils.MousePosition); foreach (var gameObject in Constrained) { Vector2 diff = (ToLookAt != null ? (Vector2)ToLookAt.transform.position : mousePos) - (Vector2)gameObject.transform.position; diff.Normalize(); gameObject.transform.rotation = Quaternion.LookRotation(diff, Vector3.up) * Quaternion.AngleAxis(-90, Vector3.up); } } public void Stop() { Quaternion[] newRotations = Constrained.Convert(_ => _.transform.rotation).ToArray(); Constrained.RemoveAll(_ => !_); for (int i = 0; i < Constrained.Count; i++) { Constrained[i].transform.rotation = initialRotations[i]; } int id = MonkeyEditorUtils.CreateUndoGroup("Look At Constrain"); for (int i = 0; i < Constrained.Count; i++) { Undo.RecordObject(Constrained[i].transform, "Look At Rotation"); Constrained[i].transform.rotation = newRotations[i]; } Undo.CollapseUndoOperations(id); } } [Command("Align Axis", QuickName = "AAI", Help = "Constrains the objects to align to the axis of another one", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT, Category = "Transform/Align")] public static void AlignAxisConstrain() { MonkeyEditorUtils.AddSceneCommand(new AxisConstrainSceneCommand(null, Selection.gameObjects, Axis.X, true)); } public class AxisConstrainSceneCommand : TimedSceneCommand { public List ToConstrain; public GameObject Reference; public Axis Axis; public bool LocalAxis; public AxisConstrainSceneCommand(GameObject reference, GameObject[] toConstrain, Axis axis, bool localAxis) : base(0) { SceneCommandName = "Align Object Axis"; this.ToConstrain = new List(toConstrain); this.Axis = axis; this.LocalAxis = localAxis; this.Reference = reference; } public override void Update() { base.Update(); if (!Reference) return; ToConstrain.RemoveAll(_ => !_); foreach (var gameObject in ToConstrain) { gameObject.transform.position = MathExt.ProjectPointOnLine(Reference.transform.position , Reference.transform.AxisToVector(Axis, LocalAxis), gameObject.transform.position); } } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("Object to Align To", ref Reference); Enum axis = Axis; DisplayEnumOption("Axis To Align On", ref axis); Axis = (Axis)axis; DisplayBoolOption("Use Local Axis", ref LocalAxis); DisplayObjectListOption("Objects Constrained", ToConstrain); } } [Command("Align On Axis Between", QuickName = "AB", Help = "Constrains the objects to align to the axis between two objects", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT, Category = "Transform/Align")] public static void AlignBetweenConstrain() { MonkeyEditorUtils.AddSceneCommand(new DualReferenceAxisConstrainSceneCommand(null, null, MonkeyEditorUtils.OrderedSelectedGameObjects.ToArray())); } public class DualReferenceAxisConstrainSceneCommand : TimedSceneCommand { private TransformUndo undo; public List ToConstrain; public GameObject Reference; public GameObject SecondReference; public DualReferenceAxisConstrainSceneCommand(GameObject reference, GameObject secondReference, GameObject[] toConstrain) : base(0) { SceneCommandName = "Align Axis Between Objects"; this.ToConstrain = new List(toConstrain); this.SecondReference = secondReference; this.Reference = reference; undo = new TransformUndo(); undo.Register(ToConstrain.Convert(_ => _.transform).ToArray()); } public override void Update() { base.Update(); if (!Reference || !SecondReference) return; ToConstrain.RemoveAll(_ => !_); foreach (var gameObject in ToConstrain) { Vector3 distVec = Reference.transform.position - SecondReference.transform.position; gameObject.transform.position = MathExt.ProjectPointOnLine(Reference.transform.position , distVec.normalized, gameObject.transform.position); } } public override void Stop() { base.Stop(); var id = MonkeyEditorUtils.CreateUndoGroup("MonKey - Align Between"); undo.RecordUndo(); Undo.CollapseUndoOperations(id); } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("First Alignment Object", ref Reference); DisplayObjectOption("Second Alignment Object", ref SecondReference); DisplayObjectListOption("Objects To Align", ToConstrain); } } [Command("Lock Distance", QuickName = "LD", Help = "Constrains the selected objects' distance to a reference one ", DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, Category = "Transform/Lock")] public static void DistanceConstrain() { MonkeyEditorUtils.AddSceneCommand(new DistanceConstraintSceneCommand(null, Selection.gameObjects, DistanceConstraintSceneCommand.ConstrainOperation.MIN, false, 1)); } [Command("Lock Distance All", QuickName = "LDA", Help = "Constrains the selected objects' distance between each other", DefaultValidation = DefaultValidation.AT_LEAST_TWO_GAME_OBJECTS, Category = "Transform/Lock")] public static void DistanceConstrainAll() { MonkeyEditorUtils.AddSceneCommand(new DistanceConstraintSceneCommand(null, MonkeyEditorUtils.OrderedSelectedGameObjects.ToArray(), DistanceConstraintSceneCommand.ConstrainOperation.MIN, true, 1)); } [Command("Lock Distance With Children", QuickName = "LDC", Help = "Constrains the selected objects' distance to the reference one's children (by default first selected)", DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, Category = "Transform/Lock")] public static void ConstrainDistanceAllChildren( [CommandParameter(Help = "The first Object who's children will constrain all the others," + " by default the first object selected", DefaultValueMethod = "FirstSelected", DefaultValueNameOverride = "First Object Selected")] GameObject reference) { List commands = new List(); foreach (Transform child in reference.GetComponentsInChildren()) { commands.Add(new DistanceConstraintSceneCommand(child.gameObject, MonkeyEditorUtils.OrderedSelectedGameObjects.Where(_ => _ != reference && _ != child.gameObject) .ToArray(), DistanceConstraintSceneCommand.ConstrainOperation.MIN, false, 1)); } MonkeyEditorUtils.AddSceneCommand(new TimedMultiSceneCommand(commands.ToArray(), 0) { SceneCommandName = "Constrain Distance Children" }); } /// /// Constrains a set of objects to respect a /// certain distance between each other /// public class DistanceConstraintSceneCommand : TimedSceneCommand { /// /// The Mathematical constrains that can be applied /// public enum ConstrainOperation { EQUAL, MIN, MAX, } /// /// the objects that will be measured against the others /// public GameObject Reference; /// /// the objects that will be constrained /// public List Constrained; /// /// Should the constrained objects /// also respect the mathematical operation between each-other /// public bool BetweenEach; /// /// The mathematical operation to consider /// public ConstrainOperation Operation; /// /// Tthe value used for the mathematical operation /// public float Value; private readonly TransformUndo undo = new TransformUndo(); /// /// /// /// /// /// /// /// public DistanceConstraintSceneCommand(GameObject reference, GameObject[] constrained, ConstrainOperation operation, bool betweenEach, float value) : base(0) { this.Reference = reference; this.Constrained = new List(constrained); this.BetweenEach = betweenEach; this.Value = value; Operation = operation; SceneCommandName = Operation.ToString().ToLower().NicifyVariableName() + " Distance Constraint"; undo.Register(constrained.Convert(_ => _.transform).ToArray()); } /// /// constrain logic /// public override void Update() { base.Update(); Constrained.RemoveAll(_ => !_); for (int i = 0; i < Constrained.Count; i++) { if (Reference) DistanceConstrain(Constrained[i], Reference, false); if (!BetweenEach) continue; for (int j = i + 1; j < Constrained.Count; j++) { DistanceConstrain(Constrained[i], Constrained[j], true); } } } /// /// /// public override void Stop() { base.Stop(); int undoID = MonkeyEditorUtils.CreateUndoGroup("Distance Lock"); undo.RecordUndo(); Undo.CollapseUndoOperations(undoID); } private void DistanceConstrain(GameObject obj, GameObject other, bool moveEach) { Vector3 distance = other.transform.position - obj.transform.position; switch (Operation) { case ConstrainOperation.EQUAL: if (Mathf.Approximately(distance.magnitude, Value)) return; break; case ConstrainOperation.MIN: if (distance.magnitude > Value) return; break; case ConstrainOperation.MAX: if (distance.magnitude < Value) return; break; default: throw new ArgumentOutOfRangeException(); } if (!moveEach) obj.transform.position = other.transform.position - distance.normalized * Value; else { Vector3 middle = (obj.transform.position + other.transform.position) / 2; obj.transform.position = middle - distance.normalized * Value * .5f; other.transform.position = middle + distance.normalized * Value * .5f; } } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("Reference Object", ref Reference); Enum constr = Operation; DisplayEnumOption("Constrain Type", ref constr); Operation = (ConstrainOperation)constr; DisplayFloatOption("Constrain Distance", ref Value); DisplayBoolOption("Constrain Between All Objects", ref BetweenEach); DisplayObjectListOption("Constrained Objects", Constrained); } } [Command("Clamp Position", "Clamps the position so that is is a multiple of the specified value", DefaultValidation = DefaultValidation.AT_LEAST_ONE_SCENE_OBJECT, QuickName = "CLP" , Category = "Transform/Clamp")] public static void ClampPositionConstrain() { MonkeyEditorUtils.AddSceneCommand( new ClampConstrainSceneCommand(Selection.gameObjects, true, false, false, 1)); } [Command("Clamp Rotation", "Clamps the rotation so that is is a multiple of the specified value", DefaultValidation = DefaultValidation.AT_LEAST_ONE_SCENE_OBJECT, QuickName = "CLR" , Category = "Transform/Clamp")] public static void ClampRotationConstrain() { MonkeyEditorUtils.AddSceneCommand( new ClampConstrainSceneCommand(Selection.gameObjects, false, true, false, 0, 1)); } [Command("Clamp Scale", "Clamps the scale so that is is a multiple of the specified value", DefaultValidation = DefaultValidation.AT_LEAST_ONE_SCENE_OBJECT, QuickName = "CLS" , Category = "Transform/Clamp")] public static void ClampScaleConstrain() { MonkeyEditorUtils.AddSceneCommand( new ClampConstrainSceneCommand(Selection.gameObjects, false, false, true, 0, 0, 1)); } public class ClampConstrainSceneCommand : TimedSceneCommand { public bool ClampPosition; public bool ClampRotation; public bool ClampScale; public float PositionClamp; public float RotationClamp; public float ScaleClamp; public GameObject[] Targets; public ClampConstrainSceneCommand(GameObject[] targets, bool clampPosition, bool clampRotation, bool clampScale, float positionClamp = 0, float rotationClamp = 0, float scaleClamp = 0) : base(0) { SceneCommandName = "Clamp Constrain"; ClampPosition = clampPosition; ClampRotation = clampRotation; ClampScale = clampScale; PositionClamp = positionClamp; RotationClamp = rotationClamp; ScaleClamp = scaleClamp; Targets = targets; } public override void Update() { base.Update(); Targets = Targets.Where(_ => _).ToArray(); foreach (var gameObject in Targets) { if (ClampPosition) { Vector3 position = gameObject.transform.position; position.x = position.x - (position.x % PositionClamp); position.y = position.y - (position.y % PositionClamp); position.z = position.z - (position.z % PositionClamp); gameObject.transform.position = position; } if (ClampRotation) { //will create limitation, good enough for now though Vector3 euler = gameObject.transform.eulerAngles; euler.x = euler.x - (euler.x % RotationClamp); euler.y = euler.y - (euler.y % RotationClamp); euler.z = euler.z - (euler.z % RotationClamp); gameObject.transform.rotation = Quaternion.Euler(euler); } if (ClampScale) { Vector3 scale = gameObject.transform.localScale; scale.x = scale.x - (scale.x % ScaleClamp); scale.y = scale.y - (scale.y % ScaleClamp); scale.z = scale.z - (scale.z % ScaleClamp); gameObject.transform.localScale = scale; } } } public override void DisplayParameters() { base.DisplayParameters(); DisplayBoolOption("Clamp Position", ref ClampPosition); DisplayFloatOption("Position Clamp Value", ref PositionClamp); DisplayBoolOption("Clamp Rotation", ref ClampPosition); DisplayFloatOption("Rotation Clamp Value", ref RotationClamp); DisplayBoolOption("Clamp Scale", ref ClampPosition); DisplayFloatOption("Scale Clamp Value", ref ScaleClamp); } } [Command("Lock In Chain", QuickName = "LC", Help = "Constrains the selected objects so they act like attached by " + "a chain from the first selected to the last", DefaultValidation = DefaultValidation.AT_LEAST_TWO_GAME_OBJECTS , Category = "Transform/Lock")] public static void ChainConstrainObjects() { MonkeyEditorUtils.AddSceneCommand( new ChainConstrain(MonkeyEditorUtils.OrderedSelectedGameObjects .Where(_ => _.scene.IsValid()).ToArray(), 0, 1)); } public class ChainConstrain : TimedSceneCommand { public List ChainElements; public float AngleTolerance; public float ChainDistance; private readonly Vector3[] previousPositions; private readonly Quaternion[] previousRotations; //TODO add an align to chain mode public ChainConstrain(GameObject[] chainElements, float angleTolerance, float chainDistance) : base(0) { SceneCommandName = "Chain Constrain"; ChainElements = new List(chainElements); AngleTolerance = angleTolerance; ChainDistance = chainDistance; previousPositions = chainElements.Convert(_ => _.transform.position).ToArray(); previousRotations = chainElements.Convert(_ => _.transform.rotation).ToArray(); } public override void Update() { base.Update(); if (ChainElements.Count < 2) return; ChainElements.RemoveAll(_ => !_); GameObject grabbedElement = null; if (Selection.gameObjects != null && Selection.gameObjects.Length > 0) { GameObject[] selection = Selection.gameObjects.Where(_ => ChainElements.Contains(_)).ToArray(); if (selection.Any()) grabbedElement = selection.First(); } int grabbedIndex = -1; if (grabbedElement) grabbedIndex = ChainElements.IndexOf(grabbedElement); if (grabbedIndex == -1 || grabbedIndex == 0) { for (int i = 0; i < ChainElements.Count; i++) { GrabNextElement(grabbedElement, i > 0 ? ChainElements[i - 1].transform : null, ChainElements[i].transform, i + 1 < ChainElements.Count ? ChainElements[i + 1].transform : null); } } else { if (grabbedElement) GrabNextElement(grabbedElement, null, grabbedElement.transform, ChainElements[grabbedIndex - 1].transform); for (int i = grabbedIndex + 1; i < ChainElements.Count; i++) { GrabNextElement(grabbedElement, i > 0 ? ChainElements[i - 1].transform : null, ChainElements[i].transform, i + 1 < ChainElements.Count ? ChainElements[i + 1].transform : null); } for (int i = grabbedIndex - 1; i >= 0; i--) { GrabNextElement(grabbedElement, i + 1 < ChainElements.Count ? ChainElements[i + 1].transform : null , ChainElements[i].transform, i > 0 ? ChainElements[i - 1].transform : null); } } } public void GrabNextElement(GameObject grabbedObject, Transform elementToFollow, Transform element, Transform previousElement) { if (elementToFollow == null) { if (Tools.pivotRotation == PivotRotation.Local && grabbedObject && grabbedObject.transform == element) return; if (previousElement && (element.position - previousElement.position).normalized.magnitude > 0) element.forward = (element.position - previousElement.position).normalized; return; } Vector3 distance = elementToFollow.position - element.transform.position; element.position = elementToFollow.position - distance.normalized * ChainDistance; Vector3 directionToLookAt; if (previousElement) { var distanceToPrevious = element.position - previousElement.position; directionToLookAt = (distanceToPrevious.normalized + distance.normalized).normalized; } else { directionToLookAt = distance.normalized; } float angle = Vector3.Angle(directionToLookAt, element.forward); if (angle >= AngleTolerance) { //TODO fix back tolerance on angle /* Quaternion currentLookAt = Quaternion.LookRotation(element.forward,element.up); Quaternion shouldLookAt = Quaternion.LookRotation(directionToLookAt,element.up); Quaternion rotation = Quaternion.RotateTowards(shouldLookAt, currentLookAt, AngleTolerance * .5f); element.forward = rotation * directionToLookAt;*/ if (directionToLookAt != Vector3.zero) element.LookAt(element.position + directionToLookAt); } } public override void DisplayParameters() { base.DisplayParameters(); DisplayFloatOption("Chaining Length", ref ChainDistance); DisplayFloatOption("Angle Tolerance", ref AngleTolerance); DisplayObjectListOption("Chain Objects", ChainElements); } public override void OnSceneGUI() { base.OnSceneGUI(); for (int i = 0; i < ChainElements.Count - 1; i++) { Handles.DrawDottedLine(ChainElements[i].transform.position, ChainElements[i + 1].transform.position, 1f); } } public override void Stop() { base.Stop(); Vector3[] newPositions = ChainElements.Convert(_ => _.transform.position).ToArray(); Quaternion[] newRotations = ChainElements.Convert(_ => _.transform.rotation).ToArray(); for (int i = 0; i < previousRotations.Length; i++) { ChainElements[i].transform.position = previousPositions[i]; ChainElements[i].transform.rotation = previousRotations[i]; } int undoIndex = MonkeyEditorUtils.CreateUndoGroup("Chain Move"); for (int i = 0; i < ChainElements.Count; i++) { Undo.RecordObject(ChainElements[i].transform, "Applying new position"); if (newPositions.Length <= i || newRotations.Length <= i) break; ChainElements[i].transform.position = newPositions[i]; ChainElements[i].transform.rotation = newRotations[i]; } Undo.CollapseUndoOperations(undoIndex); } } [Command("Move Until Collision", QuickName = "MC", Help = "Constrains the selected objects to move on the selected axis " + "until the next raycast hit", DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, Category = "Transform")] public static void ConstrainRaycastAxis() { MonkeyEditorUtils.AddSceneCommand( new AxisRayCastConstrainSceneCommand(Selection.gameObjects, DirectionAxis.DOWN, DirectionAxis.UP)); } [Command("Move Until Collision 2D", QuickName = "MC2", Help = "Constrains the selected objects to move on the selected axis " + "until the next 2D raycast hit", DefaultValidation = DefaultValidation.AT_LEAST_ONE_TRANSFORM, Category = "2D/Transform")] public static void ConstrainRaycastAxis2D() { MonkeyEditorUtils.AddSceneCommand( new AxisRayCast2DConstrainSceneCommand(Selection.gameObjects, DirectionAxis.DOWN, DirectionAxis.UP)); } public class AxisRayCastConstrainSceneCommand : TimedSceneCommand { public List Objects; //private readonly Vector3[] direction; private Vector3 direction; private readonly Vector3[] previousPositions; private readonly Quaternion[] previousRotations; private bool alignToCollision; private float toleranceDistance = 0.1f; private bool ignoreInvisibleColliders; private bool directionFromScreen; private DirectionAxis alignmentAxis; private bool active; public AxisRayCastConstrainSceneCommand(GameObject[] objects, DirectionAxis axis, DirectionAxis alignmentAxis) : base(0) { TimeBetweenUpdate = .01f; SceneCommandName = "Constrain Axis Raycast"; Objects = new List(objects); this.alignmentAxis = alignmentAxis; alignToCollision = true; direction = TransformExt.GlobalAxisToVector(axis); previousPositions = Objects.Convert(_ => _.transform.position).ToArray(); previousRotations = Objects.Convert(_ => _.transform.rotation).ToArray(); } public override void DisplayParameters() { base.DisplayParameters(); Enum en = alignmentAxis; DisplayButton(active ? "Turn Off" : "Turn On", () => { active = !active; }); DisplayDoubleOption("Time Between Updates", ref TimeBetweenUpdate); DisplayBoolOption("Ignore Invisible Colliders", ref ignoreInvisibleColliders); DisplayBoolOption("Align To Collision", ref alignToCollision); if (alignToCollision) { DisplayEnumOption("Collision Alignment Axis", ref en); alignmentAxis = (DirectionAxis)en; } DisplayBoolOption("Direction From Camera", ref directionFromScreen); if (!directionFromScreen) DisplayVectorOption("Raycast Direction", ref direction); DisplayFloatOption("Raycast Tolerance", ref toleranceDistance); DisplayObjectListOption("Objects", Objects); } public override void Update() { base.Update(); int i = 0; Objects.RemoveAll(_ => !_); if (!active) return; foreach (GameObject gameObject in Objects) { RaycastDirection(false, gameObject.transform, direction, alignToCollision, previousPositions[i], toleranceDistance, ignoreInvisibleColliders); i++; } } public void RaycastDirection(bool local, Transform t, Vector3 direction, bool alignToNormal, Vector3 originalPosition, float tolerance = 10, bool ignoreInvisible = true) { if (directionFromScreen) { var screenPoint = MonkeyEditorUtils.CurrentSceneView.camera.WorldToScreenPoint(t.position); var ray = MonkeyEditorUtils.CurrentSceneView.camera.ScreenPointToRay(screenPoint); direction = ray.direction; } var hits = Physics.RaycastAll(t.position - direction * tolerance, local ? t.TransformDirection(direction) : direction).Where(_ => !Objects.Any(s => s.GetComponentsInChildren().Contains(_.transform))) .OrderBy(_ => (_.point - (t.position - direction * tolerance)).magnitude) .ToList(); if (ignoreInvisible) { hits.RemoveAll(_ => _.transform.gameObject.GetComponent() && !_.transform.gameObject.GetComponent()); } if (hits.Count != 0) { t.AlignTransformToCollision(hits[0].point, hits[0].normal, alignToNormal, alignmentAxis); } } public override void Stop() { base.Stop(); Objects.RemoveAll(_ => !_); Vector3[] newPositions = Objects.Convert(_ => _.transform.position).ToArray(); Quaternion[] newRotations = Objects.Convert(_ => _.transform.rotation).ToArray(); for (int i = 0; i < Objects.Count; i++) { if (previousPositions.Length <= i) break; Objects[i].transform.position = previousPositions[i]; Objects[i].transform.rotation = previousRotations[i]; } int undoIndex = MonkeyEditorUtils.CreateUndoGroup("Raycast down Constrain"); for (int i = 0; i < Objects.Count; i++) { Undo.RecordObject(Objects[i].transform, "Applying new position"); Objects[i].transform.position = newPositions[i]; Objects[i].transform.rotation = newRotations[i]; } Undo.CollapseUndoOperations(undoIndex); } } public class AxisRayCast2DConstrainSceneCommand : TimedSceneCommand { public GameObject[] Objects; private readonly Vector3[] direction; private readonly Vector3[] previousPositions; private readonly Quaternion[] previousRotations; private DirectionAxis alignmentAxis; public AxisRayCast2DConstrainSceneCommand(GameObject[] objects, DirectionAxis axis, DirectionAxis alignmentAxis) : base(0) { SceneCommandName = "Constrain Axis Raycast"; this.Objects = objects; this.alignmentAxis = alignmentAxis; HideGUI = true; direction = Objects.Convert(_ => _.transform.AxisToVector(axis, false)).ToArray(); previousPositions = Objects.Convert(_ => _.transform.position).ToArray(); previousRotations = Objects.Convert(_ => _.transform.rotation).ToArray(); } public override void Update() { base.Update(); int i = 0; Objects = Objects.Where(_ => _).ToArray(); foreach (GameObject gameObject in Objects) { RaycastDirection(false, gameObject.transform, direction[i], true); i++; } } public void RaycastDirection(bool local, Transform t, Vector3 direction, bool alignToNormal, float tolerance = 10) { Vector2 originalPosition = t.position; RaycastHit2D[] hits = Physics2D.RaycastAll(t.position - direction * tolerance, local ? t.TransformDirection(direction) : direction). Where(_ => !Objects.Any(s => s.GetComponentsInChildren().Contains(_.transform))) .OrderBy(_ => (_.point - originalPosition).magnitude) .ToArray(); if (hits.Length != 0) { t.AlignTransformToCollision(hits[0].point, hits[0].normal, alignToNormal, alignmentAxis); } else { hits = Physics2D.RaycastAll(t.position - direction * tolerance, local ? t.TransformDirection(direction) : direction). Where(_ => !Objects.Contains(_.transform.gameObject)) .OrderBy(_ => (_.point - originalPosition).magnitude) .ToArray(); if (hits.Length > 0) { Undo.RecordObject(t, "movement"); t.AlignTransformToCollision(hits[0].point, hits[0].normal, alignToNormal, alignmentAxis); } } } public override void Stop() { base.Stop(); Objects = Objects.Where(_ => _).ToArray(); Vector3[] newPositions = Objects.Convert(_ => _.transform.position).ToArray(); Quaternion[] newRotations = Objects.Convert(_ => _.transform.rotation).ToArray(); for (int i = 0; i < Objects.Length; i++) { Objects[i].transform.position = previousPositions[i]; Objects[i].transform.rotation = previousRotations[i]; } int undoIndex = MonkeyEditorUtils.CreateUndoGroup("Raycast down Constrain"); for (int i = 0; i < Objects.Length; i++) { Undo.RecordObject(Objects[i].transform, "Applying new position"); Objects[i].transform.position = newPositions[i]; Objects[i].transform.rotation = newRotations[i]; } Undo.CollapseUndoOperations(undoIndex); } } [Command("Project On Plane Interactive", "Locks the selected objects on a plane defined by a point" + " and a normal", QuickName = "PPI", Category = "Transform")] public static void LockPlane() { MonkeyEditorUtils.AddSceneCommand( new LockPlaneSceneTransform(null, Axis.Y, true, Selection.gameObjects.ToArray())); } [Command("Lock Transforms", QuickName = "LT", Help = "Prevents the selected objects' transforms from moving while the command is active", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT , Category = "Transform/Lock")] public static void LockTransforms() { MonkeyEditorUtils.AddSceneCommand( new LockTransformsSceneCommand(Selection.gameObjects.Convert(_ => _.transform).ToArray())); } [Command("Lock Children Transforms", QuickName = "LCT", Help = "Prevents the selected objects' children transforms from moving while the command is active", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT , Category = "Transform/Lock")] public static void LockChildrenTransforms() { var selection = new List(); foreach (var gameObject in Selection.gameObjects) { selection.AddRange(gameObject.GetComponentsInChildren() .Where(_ => _ != gameObject.transform)); } MonkeyEditorUtils.AddSceneCommand(new LockTransformsSceneCommand(selection.ToArray())); } public static List GloballyLockedTransforms = new List(); public class LockTransformsSceneCommand : TimedSceneCommand { public List TransformsToLock; public bool ShowLockGizmos = false; struct TransformData { public Vector3 Position; public Vector3 Scale; public Quaternion Rotation; } private readonly Vector3[] lockedPosition; private readonly Vector3[] lockedScale; private readonly Quaternion[] lockedRotations; public LockTransformsSceneCommand(Transform[] transforms) : base(0) { HideGUI = true; SceneCommandName = "Lock Transforms"; TransformsToLock = new List(transforms); lockedPosition = transforms.Convert(_ => _.position).ToArray(); lockedScale = transforms.Convert(_ => _.lossyScale).ToArray(); lockedRotations = transforms.Convert(_ => _.rotation).ToArray(); GloballyLockedTransforms.AddRange(TransformsToLock); } public override void DisplayParameters() { base.DisplayParameters(); DisplayBoolOption("Show Lock Gizmos", ref ShowLockGizmos); DisplayObjectListOption("Objects To Lock", TransformsToLock); } public override void Update() { base.Update(); LockAllTransforms(); } private void LockAllTransforms() { for (int i = 0; i < TransformsToLock.Count; i++) { if (!TransformsToLock[i]) continue; TransformsToLock[i].position = lockedPosition[i]; TransformsToLock[i].rotation = lockedRotations[i]; TransformsToLock[i].SetLossyGlobalScale(lockedScale[i]); } } private readonly GUIStyle textStyle = new GUIStyle() { normal = { textColor = Color.red } }; public override void OnSceneGUI() { base.OnSceneGUI(); LockAllTransforms(); if (!ShowLockGizmos) return; foreach (var transform in TransformsToLock) { if (!transform) continue; Handles.color = Color.red.Alphaed(.2f); #if UNITY_2017_1_OR_NEWER Handles.DrawWireCube(transform.position, Vector2.one); #else Handles.DrawWireDisc(transform.position,Vector3.forward, 1); #endif Handles.Label(transform.position, "Locked", textStyle); } } /// public override void Stop() { base.Stop(); foreach (var transform in TransformsToLock) { if (!transform) continue; GloballyLockedTransforms.Remove(transform); } } } public class LockPlaneSceneTransform : TimedSceneCommand { public GameObject PlanePoint; public Axis PlaneAxis; public bool LocalAxis; public List ObjectsToConstrain; public TransformUndo Undo; public LockPlaneSceneTransform(GameObject planePoint, Axis planeAxis, bool localAxis, GameObject[] objectsToConstrain) : base(0) { PlanePoint = planePoint; PlaneAxis = planeAxis; LocalAxis = localAxis; ObjectsToConstrain = new List(objectsToConstrain); SceneCommandName = "Plane Constrain"; Undo = new TransformUndo(); Undo.Register(objectsToConstrain.Convert(_ => _.transform).ToArray()); } public override void DisplayParameters() { base.DisplayParameters(); DisplayObjectOption("Plane Point", ref PlanePoint); Enum en = PlaneAxis; DisplayEnumOption("Plane Axis", ref en); DisplayBoolOption("Local Axis", ref LocalAxis); DisplayObjectListOption("Constrained Objects", ObjectsToConstrain); } /// public override void Update() { base.Update(); foreach (var o in ObjectsToConstrain) { Vector3 projection = MathExt.ProjectPointOnPlane( PlanePoint.transform.AxisToVector(PlaneAxis, LocalAxis), PlanePoint.transform.position, o.transform.position); o.transform.position = projection; } } public override void Stop() { base.Stop(); int undoID = MonkeyEditorUtils.CreateUndoGroup("Plane Constrain"); Undo.RecordUndo(); UnityEditor.Undo.CollapseUndoOperations(undoID); } } } } #endif