// (c) Copyright HutongGames, all rights reserved. #if (UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1 || UNITY_5_2) #define UNITY_PRE_5_3 #endif #define FSM_LOG #if !PLAYMAKER_NO_UI using UnityEngine.UI; #endif using System; using System.Collections.Generic; using HutongGames.PlayMaker.Actions; using HutongGames.PlayMaker.AnimationEnums; using UnityEngine; using Random = UnityEngine.Random; #if UNITY_EDITOR using UnityEditor; #endif namespace HutongGames.PlayMaker { /// /// Helper functions to make authoring Actions simpler. /// public static class ActionHelpers { /// /// Get a small white texture /// public static Texture2D WhiteTexture { // Used to make a texture, but Unity added this: get { return Texture2D.whiteTexture; } } /// /// Common blend operations for colors /// E.g. used by TweenColor action /// /// public static Color BlendColor(ColorBlendMode blendMode, Color c1, Color c2) { switch (blendMode) { case ColorBlendMode.Normal: return Color.Lerp(c1, c2, c2.a); case ColorBlendMode.Multiply: return Color.Lerp(c1, c1 * c2, c2.a); case ColorBlendMode.Screen: var screen = Color.white - (Color.white - c1) * (Color.white - c2); return Color.Lerp(c1, screen, c2.a); default: throw new ArgumentOutOfRangeException(); } } /// /// Check the visibility of the Renderer on a GameObject /// public static bool IsVisible(GameObject go) { if (go == null) { return false; } var renderer = go.GetComponent(); return renderer != null && renderer.isVisible; } /// /// Get the GameObject targeted by an action's FsmOwnerDefault variable /// public static GameObject GetOwnerDefault(FsmStateAction action, FsmOwnerDefault ownerDefault) { return action.Fsm.GetOwnerDefaultTarget(ownerDefault); } /// /// Get the first Playmaker FSM on a game object. /// public static PlayMakerFSM GetGameObjectFsm(GameObject go, string fsmName) { if (!string.IsNullOrEmpty(fsmName)) { var fsmComponents = go.GetComponents(); foreach (var fsmComponent in fsmComponents) { if (fsmComponent.FsmName == fsmName) { return (fsmComponent); } } Debug.LogWarning("Could not find FSM: " + fsmName); } return (go.GetComponent()); } /// /// Given an array of weights, returns a randomly selected index. /// public static int GetRandomWeightedIndex(FsmFloat[] weights) { float totalWeight = 0; foreach (var t in weights) { totalWeight += t.Value; } var random = Random.Range(0, totalWeight); for (var i = 0; i < weights.Length; i++) { if (random < weights[i].Value) { return i; } random -= weights[i].Value; } return -1; } /// /// Add an animation clip to a GameObject if it has an Animation component /// public static void AddAnimationClip(GameObject go, AnimationClip animClip) { if (animClip == null) return; var animationComponent = go.GetComponent(); if (animationComponent != null) { animationComponent.AddClip(animClip, animClip.name); } } /// /// Check if an animation has finished playing. /// public static bool HasAnimationFinished(AnimationState anim, float prevTime, float currentTime) { // looping animations never finish if (anim.wrapMode == WrapMode.Loop || anim.wrapMode == WrapMode.PingPong) { return false; } // Default and Once reset to zero when done if (anim.wrapMode == WrapMode.Default || anim.wrapMode == WrapMode.Once) { if (prevTime > 0 && currentTime.Equals(0)) { return true; } } // Time keeps going up in other modes return prevTime < anim.length && currentTime >= anim.length; } // Given an FsmGameObject parameter and an FsmVector3 parameter, returns a world position. // Many actions let you define a GameObject and/or a Position... public static Vector3 GetPosition(FsmGameObject fsmGameObject, FsmVector3 fsmVector3) { Vector3 finalPos; if (fsmGameObject.Value != null) { finalPos = !fsmVector3.IsNone ? fsmGameObject.Value.transform.TransformPoint(fsmVector3.Value) : fsmGameObject.Value.transform.position; } else { finalPos = fsmVector3.Value; } return finalPos; } /* public static bool GetPosition(PositionOptions options, GameObject go, FsmGameObject target, FsmVector3 position, out Vector3 finalPos) { var validPos = false; finalPos = Vector3.zero; if (go == null || target == null || position == null) return false; switch (options) { case PositionOptions.CurrentPosition: finalPos = go.transform.position; validPos = true; break; case PositionOptions.WorldPosition: if (!position.IsNone) { finalPos = position.Value; validPos = true; } break; case PositionOptions.GameObject: if (target.Value != null) { finalPos = target.Value.transform.position; validPos = true; } break; case PositionOptions.GameObjectWithOffset: if (target != null) { finalPos = GetPosition(target, position); validPos = true; } break; case PositionOptions.WorldOffset: finalPos = go.transform.position + position.Value; validPos = true; break; case PositionOptions.LocalOffset: finalPos = go.transform.position + go.transform.InverseTransformPoint(position.Value); validPos = true; break; default: throw new ArgumentOutOfRangeException(); } return validPos; }*/ /// /// Returns a target rotation in world space given the specified parameters /// Some parameters are interpreted differently based on RotationOptions selected. /// E.g. used by TweenRotation /// /// Rotation options exposed to user /// The transform being rotated /// A potential target transform /// A potential target rotation /// public static Quaternion GetTargetRotation(RotationOptions option, Transform owner, Transform target, Vector3 rotation) { if (owner == null) return Quaternion.identity; switch (option) { case RotationOptions.CurrentRotation: return owner.rotation; case RotationOptions.WorldRotation: return Quaternion.Euler(rotation); case RotationOptions.LocalRotation: // same as world rotation if no parent if (owner.parent == null) return Quaternion.Euler(rotation); return owner.parent.rotation * Quaternion.Euler(rotation); case RotationOptions.WorldOffsetRotation: // same as rotating with global in editor return Quaternion.Euler(rotation) * owner.rotation; case RotationOptions.LocalOffsetRotation: return owner.rotation * Quaternion.Euler(rotation); case RotationOptions.MatchGameObjectRotation: if (target == null) return owner.rotation; return target.rotation * Quaternion.Euler(rotation); default: throw new ArgumentOutOfRangeException(); } //return owner.rotation; // leave as is } public static bool GetTargetRotation(RotationOptions option, Transform owner, FsmVector3 rotation, FsmGameObject target, out Quaternion targetRotation) { targetRotation = Quaternion.identity; if (owner == null || !CanEditTargetRotation(option, rotation, target)) return false; targetRotation = GetTargetRotation(option, owner, target.Value != null ? target.Value.transform : null, rotation.Value); return true; } private static bool CanEditTargetRotation(RotationOptions option, NamedVariable rotation, FsmGameObject target) { switch (option) { case RotationOptions.CurrentRotation: return false; case RotationOptions.WorldRotation: case RotationOptions.LocalRotation: case RotationOptions.WorldOffsetRotation: case RotationOptions.LocalOffsetRotation: return !rotation.IsNone; case RotationOptions.MatchGameObjectRotation: return target.Value != null; default: throw new ArgumentOutOfRangeException(); } } public static Vector3 GetTargetScale(ScaleOptions option, Transform owner, Transform target, Vector3 scale) { if (owner == null) return Vector3.one; switch (option) { case ScaleOptions.CurrentScale: return owner.localScale; case ScaleOptions.LocalScale: return scale; case ScaleOptions.MultiplyScale: return new Vector3(owner.localScale.x * scale.x, owner.localScale.y * scale.y, owner.localScale.z * scale.z); case ScaleOptions.AddToScale: return new Vector3(owner.localScale.x + scale.x, owner.localScale.y + scale.y, owner.localScale.z + scale.z); case ScaleOptions.MatchGameObject: if (target == null) return owner.localScale; return target.localScale; /* Useful...? case ScaleOptions.MatchGameObjectMultiply: if (target == null) return owner.localScale; if (scale == Vector3.one) return target.localScale; return new Vector3(target.localScale.x * scale.x, target.localScale.y * scale.y, target.localScale.z * scale.z); */ } return owner.localScale; // leave as is } public static bool GetTargetPosition(PositionOptions option, Transform owner, FsmVector3 position, FsmGameObject target, out Vector3 targetPosition) { targetPosition = Vector3.zero; if (owner == null || !IsValidTargetPosition(option, position, target)) return false; targetPosition = GetTargetPosition(option, owner, (target != null && target.Value != null) ? target.Value.transform : null, position.Value); return true; } private static bool IsValidTargetPosition(PositionOptions option, NamedVariable position, FsmGameObject target) { switch (option) { case PositionOptions.CurrentPosition: return true; case PositionOptions.WorldPosition: case PositionOptions.LocalPosition: case PositionOptions.WorldOffset: case PositionOptions.LocalOffset: return !position.IsNone; case PositionOptions.TargetGameObject: return target.Value != null; default: throw new ArgumentOutOfRangeException(); } } public static bool CanEditTargetPosition(PositionOptions option, NamedVariable position, FsmGameObject target) { switch (option) { case PositionOptions.CurrentPosition: return false; case PositionOptions.WorldPosition: case PositionOptions.LocalPosition: case PositionOptions.WorldOffset: case PositionOptions.LocalOffset: return !position.IsNone; case PositionOptions.TargetGameObject: return target.Value != null; default: throw new ArgumentOutOfRangeException(); } } public static Vector3 GetTargetPosition(PositionOptions option, Transform owner, Transform target, Vector3 position) { if (owner == null) return Vector3.zero; switch (option) { case PositionOptions.CurrentPosition: return owner.position; case PositionOptions.WorldPosition: return position; case PositionOptions.LocalPosition: if (owner.parent == null) return position; return owner.parent.TransformPoint(position); case PositionOptions.WorldOffset: return owner.position + position; case PositionOptions.LocalOffset: return owner.TransformPoint(position); case PositionOptions.TargetGameObject: if (target == null) return owner.position; if (position != Vector3.zero) return target.TransformPoint(position); return target.position; default: throw new ArgumentOutOfRangeException(); } } // Raycast helpers that cache values to minimize the number of raycasts #region MousePick public static RaycastHit mousePickInfo; static float mousePickRaycastTime; static float mousePickDistanceUsed; static int mousePickLayerMaskUsed; public static bool IsMouseOver(GameObject gameObject, float distance, int layerMask) { if (gameObject == null) return false; return gameObject == MouseOver(distance, layerMask); } public static RaycastHit MousePick(float distance, int layerMask) { if (!mousePickRaycastTime.Equals(Time.frameCount) || mousePickDistanceUsed < distance || mousePickLayerMaskUsed != layerMask) { DoMousePick(distance, layerMask); } // otherwise use cached info return mousePickInfo; } public static GameObject MouseOver(float distance, int layerMask) { if (!mousePickRaycastTime.Equals(Time.frameCount) || mousePickDistanceUsed < distance || mousePickLayerMaskUsed != layerMask) { DoMousePick(distance, layerMask); } if (mousePickInfo.collider != null) { if (mousePickInfo.distance < distance) { return mousePickInfo.collider.gameObject; } } return null; } static void DoMousePick(float distance, int layerMask) { if (Camera.main == null) { return; } var ray = Camera.main.ScreenPointToRay(Input.mousePosition); Physics.Raycast(ray, out mousePickInfo, distance, layerMask); mousePickLayerMaskUsed = layerMask; mousePickDistanceUsed = distance; mousePickRaycastTime = Time.frameCount; } #endregion public static int LayerArrayToLayerMask(FsmInt[] layers, bool invert) { var layermask = 0; foreach (var layer in layers) { layermask |= 1 << layer.Value; } if (invert) { layermask = ~layermask; } // Unity 5.3 changed this Physics property name //public const int kDefaultRaycastLayers = -5; /* #if UNITY_PRE_5_3 return layermask == 0 ? Physics.kDefaultRaycastLayers : layermask; #else return layermask == 0 ? Physics.DefaultRaycastLayers : layermask; #endif */ // HACK just return the hardcoded value to avoid separate Unity 5.3 dll // TODO Revisit in future version return layermask == 0 ? -5 : layermask; } // Does a wrap mode loop? (no finished event) public static bool IsLoopingWrapMode(WrapMode wrapMode) { return wrapMode == WrapMode.Loop || wrapMode == WrapMode.PingPong; } public static string CheckRayDistance(float rayDistance) { return rayDistance <= 0 ? "Ray Distance should be greater than zero!\n" : ""; } /// /// Check if a state responds to an event. /// Not really needed since the ErrorChecker covers this. /// public static string CheckForValidEvent(FsmState state, string eventName) { if (state == null) { return "Invalid State!"; } if (string.IsNullOrEmpty(eventName)) { return ""; } foreach (var transition in state.Fsm.GlobalTransitions) { if (transition.EventName == eventName) { return ""; } } foreach (var transition in state.Transitions) { if (transition.EventName == eventName) { return ""; } } return "Fsm will not respond to Event: " + eventName; } #region Physics setup helpers //[Obsolete("Use CheckPhysicsSetup(gameObject) instead")] public static string CheckPhysicsSetup(FsmOwnerDefault ownerDefault) { if (ownerDefault == null) return ""; return CheckPhysicsSetup(ownerDefault.GameObject.Value); } //[Obsolete("Use CheckPhysicsSetup(gameObject) instead")] public static string CheckOwnerPhysicsSetup(GameObject gameObject) { return CheckPhysicsSetup(gameObject); } public static string CheckPhysicsSetup(GameObject gameObject) { var error = string.Empty; if (gameObject != null) { if (gameObject.GetComponent() == null && gameObject.GetComponent() == null) { error += "GameObject requires RigidBody/Collider!\n"; } } return error; } //[Obsolete("Use CheckPhysics2dSetup(gameObject) instead")] public static string CheckPhysics2dSetup(FsmOwnerDefault ownerDefault) { if (ownerDefault == null) return ""; return CheckPhysics2dSetup(ownerDefault.GameObject.Value); } //[Obsolete("Use CheckPhysics2dSetup(gameObject) instead")] public static string CheckOwnerPhysics2dSetup(GameObject gameObject) { return CheckPhysics2dSetup(gameObject); } public static string CheckPhysics2dSetup(GameObject gameObject) { var error = string.Empty; if (gameObject != null) { if (gameObject.GetComponent() == null && gameObject.GetComponent() == null) { error += "GameObject requires a RigidBody2D or Collider2D component!\n"; } } return error; } #endregion #region Logging helpers public static void DebugLog(Fsm fsm, LogLevel logLevel, string text, bool sendToUnityLog = false) { #if FSM_LOG // Logging is disabled in builds so we need to handle this // case separately so actions log properly in builds if (!Application.isEditor && sendToUnityLog) { var logText = FormatUnityLogString(text); switch (logLevel) { case LogLevel.Warning: Debug.LogWarning(logText); break; case LogLevel.Error: Debug.LogError(logText); break; default: Debug.Log(logText); break; } } // Note: FsmLog.LoggingEnabled is always false in builds! // Maybe replace this with Fsm property so we can turn on/off per Fsm? if (!FsmLog.LoggingEnabled || fsm == null) { return; } switch (logLevel) { case LogLevel.Info: fsm.MyLog.LogAction(FsmLogType.Info, text, sendToUnityLog); break; case LogLevel.Warning: fsm.MyLog.LogAction(FsmLogType.Warning, text, sendToUnityLog); break; case LogLevel.Error: fsm.MyLog.LogAction(FsmLogType.Error, text, sendToUnityLog); break; } #endif } public static void LogError(string text) { DebugLog(FsmExecutionStack.ExecutingFsm, LogLevel.Error, text, true); } public static void LogWarning(string text) { DebugLog(FsmExecutionStack.ExecutingFsm, LogLevel.Warning, text, true); } /// /// Format a log string suitable for the Unity Log. /// The Unity Log lacks some context, so we bake it into the log string. /// /// Text to log. /// String formatted for the Unity Log. public static string FormatUnityLogString(string text) { if (FsmExecutionStack.ExecutingFsm == null) return text; var logString = Fsm.GetFullFsmLabel(FsmExecutionStack.ExecutingFsm); if (FsmExecutionStack.ExecutingState != null) { logString += " : " + FsmExecutionStack.ExecutingStateName; } if (FsmExecutionStack.ExecutingAction != null) { logString += FsmExecutionStack.ExecutingAction.Name; } logString += " : " + text; return logString; } #endregion #region AutoName helpers public static string GetValueLabel(INamedVariable variable) { #if UNITY_EDITOR if (variable == null) return "[null]"; if (variable.IsNone) return "[none]"; if (variable.UseVariable) return variable.Name; var rawValue = variable.RawValue; if (rawValue == null) return "null"; if (rawValue is string) return "\"" + rawValue + "\""; if (rawValue is Array) return "Array"; if (rawValue.GetType().IsValueType) return rawValue.ToString(); var label = rawValue.ToString(); var classIndex = label.IndexOf('('); if (classIndex > 0) return label.Substring(0, label.IndexOf('(')); return label; #else return ""; #endif } public static string GetValueLabel(Fsm fsm, FsmOwnerDefault ownerDefault) { if (ownerDefault == null) return "[null]"; if (ownerDefault.OwnerOption == OwnerDefaultOption.UseOwner) return "Owner"; return GetValueLabel(ownerDefault.GameObject); } /// /// ActionName : field1 field2 ... /// public static string AutoName(FsmStateAction action, params INamedVariable[] exposedFields) { return action == null ? null : AutoName(action.GetType().Name, exposedFields); } /// /// ActionName : field1 field2 ... /// public static string AutoName(string actionName, params INamedVariable[] exposedFields) { var autoName = actionName + " :"; foreach (var field in exposedFields) { autoName += " " + GetValueLabel(field); } return autoName; } /// /// ActionName : min - max /// public static string AutoNameRange(FsmStateAction action, NamedVariable min, NamedVariable max) { return action == null ? null : AutoNameRange(action.GetType().Name, min, max); } /// /// ActionName : min - max /// public static string AutoNameRange(string actionName, NamedVariable min, NamedVariable max) { return actionName + " : " + GetValueLabel(min) + " - " + GetValueLabel(max); } /// /// ActionName : var = value /// public static string AutoNameSetVar(FsmStateAction action, NamedVariable var, NamedVariable value) { return action == null ? null : AutoNameSetVar(action.GetType().Name, var, value); } /// /// ActionName : var = value /// public static string AutoNameSetVar(string actionName, NamedVariable var, NamedVariable value) { return actionName + " : " + GetValueLabel(var) + " = " + GetValueLabel(value); } /// /// [-Convert]ActionName : fromVar to toVar /// public static string AutoNameConvert(FsmStateAction action, NamedVariable fromVariable, NamedVariable toVariable) { return action == null ? null : AutoNameConvert(action.GetType().Name, fromVariable, toVariable); } /// /// [-Convert]ActionName : fromVar to toVar /// public static string AutoNameConvert(string actionName, NamedVariable fromVariable, NamedVariable toVariable) { return actionName.Replace("Convert","") + " : " + GetValueLabel(fromVariable) + " to " + GetValueLabel(toVariable); } /// /// ActionName : property -> store /// public static string AutoNameGetProperty(FsmStateAction action, NamedVariable property, NamedVariable store) { return action == null ? null : AutoNameGetProperty(action.GetType().Name, property, store); } /// /// ActionName : property -> store /// public static string AutoNameGetProperty(string actionName, NamedVariable property, NamedVariable store) { return actionName + " : " + GetValueLabel(property) + " -> " + GetValueLabel(store); } #endregion #region Editor helpers #if UNITY_EDITOR /// /// Gets a rect that fits in the controls column of an inspector. /// /// Desired height. public static Rect GetControlPreviewRect(float height) { var rect = GUILayoutUtility.GetRect(100f, 3000f, height, height); var labelWidth = EditorGUIUtility.labelWidth; rect.x += labelWidth + 5; rect.width -= labelWidth + 30; return rect; } public static Vector3 DoTargetPositionHandle(Vector3 worldPos, PositionOptions option, Transform owner, FsmGameObject target) { //var worldPos = GetTargetPosition(option, owner, target, position); EditorGUI.BeginChangeCheck(); var rotation = Quaternion.identity; var newPos = worldPos; switch (option) { case PositionOptions.CurrentPosition: break; case PositionOptions.WorldPosition: newPos = Handles.PositionHandle(worldPos, rotation); break; case PositionOptions.LocalPosition: if (owner.parent != null) { rotation = owner.parent.transform.rotation; newPos = owner.parent.InverseTransformPoint(Handles.PositionHandle(worldPos, rotation)); } else { newPos = Handles.PositionHandle(worldPos, rotation); } break; case PositionOptions.WorldOffset: newPos = Handles.PositionHandle(worldPos, rotation) - owner.position; break; case PositionOptions.LocalOffset: rotation = owner.rotation; newPos = owner.InverseTransformPoint(Handles.PositionHandle(worldPos, rotation)) ; break; case PositionOptions.TargetGameObject: if (target.Value == null) return worldPos; rotation = target.Value.transform.rotation; newPos = target.Value.transform.InverseTransformPoint(Handles.PositionHandle(worldPos, rotation)); break; default: throw new ArgumentOutOfRangeException(); } if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(owner, "Move Scene Gizmo"); } return newPos; } /// /// Draws a Position Handle in the scene using a combination of GameObject and Position values. /// If a GameObject is specified, the Position is a local offset. /// If no GameObject is specified, the Position is a world position. /// Many actions use this setup. /// /// /// /// /// public static Vector3 PositionHandle(UnityEngine.Object owner, GameObject go, Vector3 position) { EditorGUI.BeginChangeCheck(); Transform transform = null; var rotation = Quaternion.identity; var worldPos = GetPosition(go, position); if (go != null) { transform = go.transform; rotation = transform.rotation; Handles.Label(transform.position, go.name, "Box"); Handles.DrawDottedLine(transform.position, worldPos, 2f); } worldPos = Handles.PositionHandle(worldPos, rotation); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(owner, "Move Scene Gizmo"); } return transform != null ? transform.InverseTransformPoint(worldPos) : worldPos; } /// /// Draws an arrow in the scene. /// Useful for actions that set a direction. /// public static void DrawArrow(Vector3 fromPos, Vector3 toPos, Color color, float arrowScale = 0.2f) { var direction = toPos - fromPos; if (direction.sqrMagnitude < 0.0001f) return; var lookAtRotation = Quaternion.LookRotation(direction); var distance = Vector3.Distance(fromPos, toPos); var handleSize = HandleUtility.GetHandleSize(toPos); var arrowSize = handleSize * arrowScale; var originalColor = Handles.color; Handles.color = color; Handles.DrawLine(fromPos, toPos); #if UNITY_5_5_OR_NEWER Handles.ConeHandleCap(0, fromPos + direction.normalized * (distance - arrowSize), lookAtRotation, arrowSize, EventType.Repaint); // fudge factor to position cap correctly #else Handles.ConeCap(0, fromPos + direction.normalized * (distance - arrowSize), lookAtRotation, arrowSize); // fudge factor to position cap correctly #endif Handles.color = originalColor; } /// /// Get a mesh that can be used by Gizmos.DrawMesh to preview the mesh while editing. /// E.g. to preview a GameObject moving to a target /// public static Mesh GetPreviewMesh(GameObject go) { if (go == null) return null; var meshFilters = go.GetComponentsInChildren(false); if (meshFilters.Length == 0) return null; var combineList = new List(); foreach (var meshFilter in meshFilters) { var combine = new CombineInstance { mesh = meshFilter.sharedMesh, transform = meshFilter.transform.localToWorldMatrix }; combineList.Add(combine); } var combinedMesh = new Mesh(); combinedMesh.CombineMeshes(combineList.ToArray()); return combinedMesh; } /// /// Single color version of Handles.ScaleHandle. /// Useful when you have multiple editors (e.g. TweenScale) /// Note, does not handle value of 0 very well (fix?) /// public static Vector3 SingleColorScaleHandle(GameObject go, Vector3 scale, float handleSize, Color color) { var matrix = Handles.matrix; Handles.matrix = go.transform.localToWorldMatrix; Handles.matrix *= Matrix4x4.Inverse(Matrix4x4.Scale(go.transform.localScale)); var tempColor = Handles.color; Handles.color = color; var scaleX = Handles.ScaleSlider(scale.x, Vector3.zero, -Vector3.left, Quaternion.identity, handleSize, 0f); var scaleY = Handles.ScaleSlider(scale.y, Vector3.zero, -Vector3.down, Quaternion.identity, handleSize, 0f); var scaleZ = Handles.ScaleSlider(scale.z, Vector3.zero, -Vector3.back, Quaternion.identity, handleSize, 0f); Handles.color = tempColor; Handles.matrix = matrix; scale.Set(scaleX, scaleY, scaleZ); return scale; } /// /// Get a local bounding box for a GameObject /// public static Bounds GetLocalBounds(GameObject gameObject) { // See GetLocalBounds in InternalEditorUtilityBindings.gen.cs in unity c# ref projects var rectTransform = gameObject.GetComponent(); if (rectTransform) { return new Bounds(rectTransform.rect.center, rectTransform.rect.size); } var renderer = gameObject.GetComponent(); if (renderer is MeshRenderer) { var filter = renderer.GetComponent(); if (filter != null && filter.sharedMesh != null) return filter.sharedMesh.bounds; } var spriteRenderer = renderer as SpriteRenderer; if (spriteRenderer != null) { return spriteRenderer.bounds; } return new Bounds(Vector3.zero, Vector3.zero); } /// /// Draw wire bounding box for Transform. /// Optionally scale bounding box. /// public static void DrawWireBounds(Transform transform, Vector3 scale, Color color) { var matrix = Handles.matrix; Handles.matrix = transform.localToWorldMatrix; var bounds = GetLocalBounds(transform.gameObject); var size = bounds.size; size.Set(size.x * scale.x, size.y * scale.y, size.z * scale.z); DrawWireCube(Vector3.zero, size, color); Handles.matrix = matrix; } /// /// Draw wireframe bounding box around object with optional rotation (for editing gizmos) /// public static void DrawWireBounds(Transform transform, Quaternion rotate, Color color) { var matrix = Handles.matrix; Handles.matrix = Matrix4x4.TRS(transform.position, rotate, transform.lossyScale); DrawWireCube(Vector3.zero, GetLocalBounds(transform.gameObject).size, color); Handles.matrix = matrix; } public static void DrawWireBounds(Transform transform, Vector3 position, Quaternion rotation, Color color) { var matrix = Handles.matrix; Handles.matrix = Matrix4x4.TRS(position, rotation, transform.lossyScale); DrawWireCube(Vector3.zero, GetLocalBounds(transform.gameObject).size, color); Handles.matrix = matrix; } // Creates a rotation matrix. Note: Assumes unit quaternion public static Matrix4x4 Matrix4X4Rotate(Quaternion q) { // Pre-calculate coordinate products var x = q.x * 2.0F; var y = q.y * 2.0F; var z = q.z * 2.0F; var xx = q.x * x; var yy = q.y * y; var zz = q.z * z; var xy = q.x * y; var xz = q.x * z; var yz = q.y * z; var wx = q.w * x; var wy = q.w * y; var wz = q.w * z; // Calculate 3x3 matrix from orthonormal basis Matrix4x4 m; m.m00 = 1.0f - (yy + zz); m.m10 = xy + wz; m.m20 = xz - wy; m.m30 = 0.0F; m.m01 = xy - wz; m.m11 = 1.0f - (xx + zz); m.m21 = yz + wx; m.m31 = 0.0F; m.m02 = xz + wy; m.m12 = yz - wx; m.m22 = 1.0f - (xx + yy); m.m32 = 0.0F; m.m03 = 0.0F; m.m13 = 0.0F; m.m23 = 0.0F; m.m33 = 1.0F; return m; } /// /// Draw wireframe bounding box around object with optional translate, rotate, and scale (for editing gizmos) /// public static void DrawWireBounds(Transform transform, Vector3 translate, Quaternion rotate, Vector3 scale, Color color) { var matrix = Handles.matrix; Handles.matrix = transform.localToWorldMatrix; Handles.matrix *= Matrix4x4.TRS(translate, rotate, scale); DrawWireCube(Vector3.zero, GetLocalBounds(transform.gameObject).size, color); Handles.matrix = matrix; } /// /// Similar to Gizmos.DrawWireCube but can be used in editor code. /// public static void DrawWireCube(Vector3 position, Vector3 size, Color color) { var originalColor = Handles.color; Handles.color = color; var half = size / 2; // draw front Handles.DrawLine(position + new Vector3(-half.x, -half.y, half.z), position + new Vector3(half.x, -half.y, half.z)); Handles.DrawLine(position + new Vector3(-half.x, -half.y, half.z), position + new Vector3(-half.x, half.y, half.z)); Handles.DrawLine(position + new Vector3(half.x, half.y, half.z), position + new Vector3(half.x, -half.y, half.z)); Handles.DrawLine(position + new Vector3(half.x, half.y, half.z), position + new Vector3(-half.x, half.y, half.z)); // draw back Handles.DrawLine(position + new Vector3(-half.x, -half.y, -half.z), position + new Vector3(half.x, -half.y, -half.z)); Handles.DrawLine(position + new Vector3(-half.x, -half.y, -half.z), position + new Vector3(-half.x, half.y, -half.z)); Handles.DrawLine(position + new Vector3(half.x, half.y, -half.z), position + new Vector3(half.x, -half.y, -half.z)); Handles.DrawLine(position + new Vector3(half.x, half.y, -half.z), position + new Vector3(-half.x, half.y, -half.z)); // draw corners Handles.DrawLine(position + new Vector3(-half.x, -half.y, -half.z), position + new Vector3(-half.x, -half.y, half.z)); Handles.DrawLine(position + new Vector3(half.x, -half.y, -half.z), position + new Vector3(half.x, -half.y, half.z)); Handles.DrawLine(position + new Vector3(-half.x, half.y, -half.z), position + new Vector3(-half.x, half.y, half.z)); Handles.DrawLine(position + new Vector3(half.x, half.y, -half.z), position + new Vector3(half.x, half.y, half.z)); Handles.color = originalColor; } #endif #endregion #region Obsolete /// /// Actions should use this for consistent error messages. /// Error will contain action name and full FSM path. /// [Obsolete("Use LogError instead.")] public static void RuntimeError(FsmStateAction action, string error) { action.LogError(action + " : " + error); } #endregion } }