1214 lines
42 KiB
C#
1214 lines
42 KiB
C#
// (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
|
|
{
|
|
|
|
/// <summary>
|
|
/// Helper functions to make authoring Actions simpler.
|
|
/// </summary>
|
|
public static class ActionHelpers
|
|
{
|
|
/// <summary>
|
|
/// Get a small white texture
|
|
/// </summary>
|
|
public static Texture2D WhiteTexture
|
|
{
|
|
// Used to make a texture, but Unity added this:
|
|
get { return Texture2D.whiteTexture; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Common blend operations for colors
|
|
/// E.g. used by TweenColor action
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check the visibility of the Renderer on a GameObject
|
|
/// </summary>
|
|
public static bool IsVisible(GameObject go)
|
|
{
|
|
if (go == null)
|
|
{
|
|
return false;
|
|
}
|
|
var renderer = go.GetComponent<Renderer>();
|
|
return renderer != null && renderer.isVisible;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the GameObject targeted by an action's FsmOwnerDefault variable
|
|
/// </summary>
|
|
public static GameObject GetOwnerDefault(FsmStateAction action, FsmOwnerDefault ownerDefault)
|
|
{
|
|
return action.Fsm.GetOwnerDefaultTarget(ownerDefault);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the first Playmaker FSM on a game object.
|
|
/// </summary>
|
|
public static PlayMakerFSM GetGameObjectFsm(GameObject go, string fsmName)
|
|
{
|
|
if (!string.IsNullOrEmpty(fsmName))
|
|
{
|
|
var fsmComponents = go.GetComponents<PlayMakerFSM>();
|
|
|
|
foreach (var fsmComponent in fsmComponents)
|
|
{
|
|
if (fsmComponent.FsmName == fsmName)
|
|
{
|
|
return (fsmComponent);
|
|
}
|
|
}
|
|
|
|
Debug.LogWarning("Could not find FSM: " + fsmName);
|
|
}
|
|
|
|
return (go.GetComponent<PlayMakerFSM>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given an array of weights, returns a randomly selected index.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an animation clip to a GameObject if it has an Animation component
|
|
/// </summary>
|
|
public static void AddAnimationClip(GameObject go, AnimationClip animClip)
|
|
{
|
|
if (animClip == null) return;
|
|
var animationComponent = go.GetComponent<Animation>();
|
|
if (animationComponent != null)
|
|
{
|
|
animationComponent.AddClip(animClip, animClip.name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if an animation has finished playing.
|
|
/// </summary>
|
|
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;
|
|
}*/
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="option">Rotation options exposed to user</param>
|
|
/// <param name="owner">The transform being rotated</param>
|
|
/// <param name="target">A potential target transform</param>
|
|
/// <param name="rotation">A potential target rotation</param>
|
|
/// <returns></returns>
|
|
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" : "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a state responds to an event.
|
|
/// Not really needed since the ErrorChecker covers this.
|
|
/// </summary>
|
|
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<Collider>() == null && gameObject.GetComponent<Rigidbody>() == 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<Collider2D>() == null && gameObject.GetComponent<Rigidbody2D>() == 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format a log string suitable for the Unity Log.
|
|
/// The Unity Log lacks some context, so we bake it into the log string.
|
|
/// </summary>
|
|
/// <param name="text">Text to log.</param>
|
|
/// <returns>String formatted for the Unity Log.</returns>
|
|
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);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// ActionName : field1 field2 ...
|
|
/// </summary>
|
|
public static string AutoName(FsmStateAction action, params INamedVariable[] exposedFields)
|
|
{
|
|
return action == null ? null : AutoName(action.GetType().Name, exposedFields);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : field1 field2 ...
|
|
/// </summary>
|
|
public static string AutoName(string actionName, params INamedVariable[] exposedFields)
|
|
{
|
|
var autoName = actionName + " :";
|
|
foreach (var field in exposedFields)
|
|
{
|
|
autoName += " " + GetValueLabel(field);
|
|
}
|
|
|
|
return autoName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : min - max
|
|
/// </summary>
|
|
public static string AutoNameRange(FsmStateAction action, NamedVariable min, NamedVariable max)
|
|
{
|
|
return action == null ? null : AutoNameRange(action.GetType().Name, min, max);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : min - max
|
|
/// </summary>
|
|
public static string AutoNameRange(string actionName, NamedVariable min, NamedVariable max)
|
|
{
|
|
return actionName + " : " + GetValueLabel(min) + " - " + GetValueLabel(max);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : var = value
|
|
/// </summary>
|
|
public static string AutoNameSetVar(FsmStateAction action, NamedVariable var, NamedVariable value)
|
|
{
|
|
return action == null ? null : AutoNameSetVar(action.GetType().Name, var, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : var = value
|
|
/// </summary>
|
|
public static string AutoNameSetVar(string actionName, NamedVariable var, NamedVariable value)
|
|
{
|
|
return actionName + " : " + GetValueLabel(var) + " = " + GetValueLabel(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// [-Convert]ActionName : fromVar to toVar
|
|
/// </summary>
|
|
public static string AutoNameConvert(FsmStateAction action, NamedVariable fromVariable, NamedVariable toVariable)
|
|
{
|
|
return action == null ? null : AutoNameConvert(action.GetType().Name, fromVariable, toVariable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// [-Convert]ActionName : fromVar to toVar
|
|
/// </summary>
|
|
public static string AutoNameConvert(string actionName, NamedVariable fromVariable, NamedVariable toVariable)
|
|
{
|
|
return actionName.Replace("Convert","") + " : " + GetValueLabel(fromVariable) + " to " + GetValueLabel(toVariable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : property -> store
|
|
/// </summary>
|
|
public static string AutoNameGetProperty(FsmStateAction action, NamedVariable property, NamedVariable store)
|
|
{
|
|
return action == null ? null : AutoNameGetProperty(action.GetType().Name, property, store);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionName : property -> store
|
|
/// </summary>
|
|
public static string AutoNameGetProperty(string actionName, NamedVariable property, NamedVariable store)
|
|
{
|
|
return actionName + " : " + GetValueLabel(property) + " -> " + GetValueLabel(store);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Editor helpers
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
/// <summary>
|
|
/// Gets a rect that fits in the controls column of an inspector.
|
|
/// </summary>
|
|
/// <param name="height">Desired height.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="owner"></param>
|
|
/// <param name="go"></param>
|
|
/// <param name="position"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws an arrow in the scene.
|
|
/// Useful for actions that set a direction.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public static Mesh GetPreviewMesh(GameObject go)
|
|
{
|
|
if (go == null) return null;
|
|
|
|
var meshFilters = go.GetComponentsInChildren<MeshFilter>(false);
|
|
if (meshFilters.Length == 0) return null;
|
|
|
|
var combineList = new List<CombineInstance>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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?)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a local bounding box for a GameObject
|
|
/// </summary>
|
|
public static Bounds GetLocalBounds(GameObject gameObject)
|
|
{
|
|
// See GetLocalBounds in InternalEditorUtilityBindings.gen.cs in unity c# ref projects
|
|
|
|
var rectTransform = gameObject.GetComponent<RectTransform>();
|
|
if (rectTransform)
|
|
{
|
|
return new Bounds(rectTransform.rect.center, rectTransform.rect.size);
|
|
}
|
|
|
|
var renderer = gameObject.GetComponent<Renderer>();
|
|
if (renderer is MeshRenderer)
|
|
{
|
|
var filter = renderer.GetComponent<MeshFilter>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw wire bounding box for Transform.
|
|
/// Optionally scale bounding box.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw wireframe bounding box around object with optional rotation (for editing gizmos)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw wireframe bounding box around object with optional translate, rotate, and scale (for editing gizmos)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Similar to Gizmos.DrawWireCube but can be used in editor code.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Actions should use this for consistent error messages.
|
|
/// Error will contain action name and full FSM path.
|
|
/// </summary>
|
|
[Obsolete("Use LogError instead.")]
|
|
public static void RuntimeError(FsmStateAction action, string error)
|
|
{
|
|
action.LogError(action + " : " + error);
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|