chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class AnimatorControl
|
||||
{
|
||||
public static object Play(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
int layer = @params["layer"]?.ToObject<int>() ?? -1;
|
||||
|
||||
Undo.RecordObject(animator, "Play Animation State");
|
||||
animator.Play(stateName, layer);
|
||||
|
||||
return new { success = true, message = $"Playing state '{stateName}' on '{go.name}'" };
|
||||
}
|
||||
|
||||
public static object Crossfade(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
float duration = @params["duration"]?.ToObject<float>() ?? 0.25f;
|
||||
int layer = @params["layer"]?.ToObject<int>() ?? -1;
|
||||
|
||||
Undo.RecordObject(animator, "Crossfade Animation State");
|
||||
animator.CrossFade(stateName, duration, layer);
|
||||
|
||||
return new { success = true, message = $"Crossfading to '{stateName}' over {duration}s on '{go.name}'" };
|
||||
}
|
||||
|
||||
public static object SetParameter(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
string paramName = @params["parameterName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(paramName))
|
||||
return new { success = false, message = "'parameterName' is required" };
|
||||
|
||||
string paramType = @params["parameterType"]?.ToString()?.ToLowerInvariant();
|
||||
|
||||
// Auto-detect type if not specified
|
||||
if (string.IsNullOrEmpty(paramType))
|
||||
{
|
||||
for (int i = 0; i < animator.parameterCount; i++)
|
||||
{
|
||||
var p = animator.GetParameter(i);
|
||||
if (p.name == paramName)
|
||||
{
|
||||
paramType = p.type.ToString().ToLowerInvariant();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(paramType))
|
||||
return new { success = false, message = $"Parameter '{paramName}' not found. Specify 'parameterType' explicitly or check the parameter name." };
|
||||
}
|
||||
|
||||
JToken valueToken = @params["value"];
|
||||
|
||||
// In Edit mode, runtime Animator.SetFloat/SetInteger/SetBool are no-ops because
|
||||
// the Animator graph isn't active. Instead, modify the controller asset's default
|
||||
// parameter values so changes actually persist.
|
||||
bool isPlaying = Application.isPlaying;
|
||||
|
||||
if (isPlaying)
|
||||
{
|
||||
Undo.RecordObject(animator, $"Set Animator Parameter {paramName}");
|
||||
|
||||
switch (paramType)
|
||||
{
|
||||
case "float":
|
||||
float fVal = valueToken?.ToObject<float>() ?? 0f;
|
||||
animator.SetFloat(paramName, fVal);
|
||||
return new { success = true, message = $"Set float '{paramName}' = {fVal}" };
|
||||
|
||||
case "int":
|
||||
case "integer":
|
||||
int iVal = valueToken?.ToObject<int>() ?? 0;
|
||||
animator.SetInteger(paramName, iVal);
|
||||
return new { success = true, message = $"Set int '{paramName}' = {iVal}" };
|
||||
|
||||
case "bool":
|
||||
case "boolean":
|
||||
bool bVal = valueToken?.ToObject<bool>() ?? false;
|
||||
animator.SetBool(paramName, bVal);
|
||||
return new { success = true, message = $"Set bool '{paramName}' = {bVal}" };
|
||||
|
||||
case "trigger":
|
||||
animator.SetTrigger(paramName);
|
||||
return new { success = true, message = $"Set trigger '{paramName}'" };
|
||||
|
||||
default:
|
||||
return new { success = false, message = $"Unknown parameter type: {paramType}. Valid: float, int, bool, trigger" };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Edit mode: modify the AnimatorController asset's default parameter values
|
||||
var controller = animator.runtimeAnimatorController as AnimatorController;
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"No AnimatorController assigned to Animator on '{go.name}'. Cannot set parameter defaults in Edit mode." };
|
||||
|
||||
var allParams = controller.parameters;
|
||||
int paramIndex = -1;
|
||||
for (int i = 0; i < allParams.Length; i++)
|
||||
{
|
||||
if (allParams[i].name == paramName)
|
||||
{
|
||||
paramIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (paramIndex < 0)
|
||||
return new { success = false, message = $"Parameter '{paramName}' not found on controller '{controller.name}'." };
|
||||
|
||||
Undo.RecordObject(controller, $"Set Parameter Default {paramName}");
|
||||
|
||||
switch (paramType)
|
||||
{
|
||||
case "float":
|
||||
float fVal = valueToken?.ToObject<float>() ?? 0f;
|
||||
allParams[paramIndex].defaultFloat = fVal;
|
||||
controller.parameters = allParams;
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
return new { success = true, message = $"Set float '{paramName}' = {fVal} (default value, Edit mode)" };
|
||||
|
||||
case "int":
|
||||
case "integer":
|
||||
int iVal = valueToken?.ToObject<int>() ?? 0;
|
||||
allParams[paramIndex].defaultInt = iVal;
|
||||
controller.parameters = allParams;
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
return new { success = true, message = $"Set int '{paramName}' = {iVal} (default value, Edit mode)" };
|
||||
|
||||
case "bool":
|
||||
case "boolean":
|
||||
bool bVal = valueToken?.ToObject<bool>() ?? false;
|
||||
allParams[paramIndex].defaultBool = bVal;
|
||||
controller.parameters = allParams;
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
return new { success = true, message = $"Set bool '{paramName}' = {bVal} (default value, Edit mode)" };
|
||||
|
||||
case "trigger":
|
||||
return new { success = true, message = $"Trigger '{paramName}' noted (triggers are runtime-only, no default to set)" };
|
||||
|
||||
default:
|
||||
return new { success = false, message = $"Unknown parameter type: {paramType}. Valid: float, int, bool, trigger" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static object SetSpeed(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
float speed = @params["speed"]?.ToObject<float>() ?? 1f;
|
||||
|
||||
Undo.RecordObject(animator, "Set Animator Speed");
|
||||
animator.speed = speed;
|
||||
|
||||
return new { success = true, message = $"Set animator speed to {speed} on '{go.name}'" };
|
||||
}
|
||||
|
||||
public static object SetEnabled(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
bool enabled = @params["enabled"]?.ToObject<bool>() ?? true;
|
||||
|
||||
Undo.RecordObject(animator, "Set Animator Enabled");
|
||||
animator.enabled = enabled;
|
||||
|
||||
return new { success = true, message = $"Animator {(enabled ? "enabled" : "disabled")} on '{go.name}'" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac285365908a4fb39f775b2af5edc60b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class AnimatorRead
|
||||
{
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
var parameters = new List<object>();
|
||||
for (int i = 0; i < animator.parameterCount; i++)
|
||||
{
|
||||
var p = animator.GetParameter(i);
|
||||
parameters.Add(new
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type.ToString(),
|
||||
defaultFloat = p.defaultFloat,
|
||||
defaultInt = p.defaultInt,
|
||||
defaultBool = p.defaultBool
|
||||
});
|
||||
}
|
||||
|
||||
var layers = new List<object>();
|
||||
for (int i = 0; i < animator.layerCount; i++)
|
||||
{
|
||||
var stateInfo = animator.IsInTransition(i)
|
||||
? animator.GetNextAnimatorStateInfo(i)
|
||||
: animator.GetCurrentAnimatorStateInfo(i);
|
||||
|
||||
layers.Add(new
|
||||
{
|
||||
index = i,
|
||||
name = animator.GetLayerName(i),
|
||||
weight = animator.GetLayerWeight(i),
|
||||
currentStateHash = stateInfo.fullPathHash,
|
||||
currentStateNormalizedTime = stateInfo.normalizedTime,
|
||||
currentStateLength = stateInfo.length,
|
||||
isInTransition = animator.IsInTransition(i)
|
||||
});
|
||||
}
|
||||
|
||||
var clips = new List<object>();
|
||||
if (animator.runtimeAnimatorController != null)
|
||||
{
|
||||
foreach (var clip in animator.runtimeAnimatorController.animationClips)
|
||||
{
|
||||
clips.Add(new
|
||||
{
|
||||
name = clip.name,
|
||||
length = clip.length,
|
||||
frameRate = clip.frameRate,
|
||||
isLooping = clip.isLooping,
|
||||
wrapMode = clip.wrapMode.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
gameObject = go.name,
|
||||
enabled = animator.enabled,
|
||||
speed = animator.speed,
|
||||
hasController = animator.runtimeAnimatorController != null,
|
||||
controllerName = animator.runtimeAnimatorController?.name,
|
||||
applyRootMotion = animator.applyRootMotion,
|
||||
updateMode = animator.updateMode.ToString(),
|
||||
cullingMode = animator.cullingMode.ToString(),
|
||||
parameterCount = animator.parameterCount,
|
||||
layerCount = animator.layerCount,
|
||||
parameters,
|
||||
layers,
|
||||
clips
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object GetParameter(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
return new { success = false, message = $"No Animator component on '{go.name}'" };
|
||||
|
||||
string paramName = @params["parameterName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(paramName))
|
||||
return new { success = false, message = "'parameterName' is required" };
|
||||
|
||||
AnimatorControllerParameter found = null;
|
||||
for (int i = 0; i < animator.parameterCount; i++)
|
||||
{
|
||||
var p = animator.GetParameter(i);
|
||||
if (p.name == paramName)
|
||||
{
|
||||
found = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == null)
|
||||
return new { success = false, message = $"Parameter '{paramName}' not found on Animator" };
|
||||
|
||||
object value;
|
||||
switch (found.type)
|
||||
{
|
||||
case AnimatorControllerParameterType.Float:
|
||||
value = animator.GetFloat(paramName);
|
||||
break;
|
||||
case AnimatorControllerParameterType.Int:
|
||||
value = animator.GetInteger(paramName);
|
||||
break;
|
||||
case AnimatorControllerParameterType.Bool:
|
||||
value = animator.GetBool(paramName);
|
||||
break;
|
||||
case AnimatorControllerParameterType.Trigger:
|
||||
value = animator.GetBool(paramName);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
name = found.name,
|
||||
type = found.type.ToString(),
|
||||
value
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66c604dff493453da1bc8cb6032ebf35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,637 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class ClipCreate
|
||||
{
|
||||
public static object Create(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required (e.g. 'Assets/Animations/Walk.anim')" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
if (!clipPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
clipPath += ".anim";
|
||||
|
||||
// Ensure directory exists
|
||||
string dir = Path.GetDirectoryName(clipPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
|
||||
{
|
||||
CreateFoldersRecursive(dir);
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
var existing = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (existing != null)
|
||||
return new { success = false, message = $"AnimationClip already exists at '{clipPath}'. Delete it first or use a different path." };
|
||||
|
||||
var clip = new AnimationClip();
|
||||
string name = @params["name"]?.ToString();
|
||||
clip.name = !string.IsNullOrEmpty(name)
|
||||
? name
|
||||
: Path.GetFileNameWithoutExtension(clipPath);
|
||||
|
||||
float length = @params["length"]?.ToObject<float>() ?? 1f;
|
||||
clip.frameRate = @params["frameRate"]?.ToObject<float>() ?? 60f;
|
||||
|
||||
bool loop = @params["loop"]?.ToObject<bool>() ?? false;
|
||||
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
||||
settings.loopTime = loop;
|
||||
settings.stopTime = length;
|
||||
AnimationUtility.SetAnimationClipSettings(clip, settings);
|
||||
|
||||
AssetDatabase.CreateAsset(clip, clipPath);
|
||||
|
||||
// Set m_WrapMode via SerializedObject — clip.wrapMode is a runtime property
|
||||
// that doesn't serialize to m_WrapMode, so we set it directly for the legacy system
|
||||
if (loop)
|
||||
{
|
||||
var so = new SerializedObject(clip);
|
||||
var wrapProp = so.FindProperty("m_WrapMode");
|
||||
if (wrapProp != null)
|
||||
{
|
||||
wrapProp.intValue = (int)WrapMode.Loop;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created AnimationClip at '{clipPath}'",
|
||||
data = new
|
||||
{
|
||||
path = clipPath,
|
||||
name = clip.name,
|
||||
length,
|
||||
frameRate = clip.frameRate,
|
||||
isLooping = loop
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
||||
var bindings = AnimationUtility.GetCurveBindings(clip);
|
||||
|
||||
var curves = new List<object>();
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||||
curves.Add(new
|
||||
{
|
||||
path = binding.path,
|
||||
propertyName = binding.propertyName,
|
||||
type = binding.type.Name,
|
||||
keyCount = curve?.length ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
var events = AnimationUtility.GetAnimationEvents(clip);
|
||||
var eventList = events.Select(e => new
|
||||
{
|
||||
time = e.time,
|
||||
functionName = e.functionName,
|
||||
stringParameter = e.stringParameter,
|
||||
floatParameter = e.floatParameter,
|
||||
intParameter = e.intParameter
|
||||
}).ToArray();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
path = clipPath,
|
||||
name = clip.name,
|
||||
length = clip.length,
|
||||
frameRate = clip.frameRate,
|
||||
isLooping = settings.loopTime,
|
||||
wrapMode = clip.wrapMode.ToString(),
|
||||
curveCount = bindings.Length,
|
||||
curves,
|
||||
eventCount = events.Length,
|
||||
events = eventList
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AddCurve(JObject @params)
|
||||
{
|
||||
return SetOrAddCurve(@params, append: true);
|
||||
}
|
||||
|
||||
public static object SetCurve(JObject @params)
|
||||
{
|
||||
return SetOrAddCurve(@params, append: false);
|
||||
}
|
||||
|
||||
private static object SetOrAddCurve(JObject @params, bool append)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
string propertyPath = @params["propertyPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(propertyPath))
|
||||
return new { success = false, message = "'propertyPath' is required (e.g. 'localPosition.x')" };
|
||||
|
||||
string typeName = @params["type"]?.ToString() ?? "Transform";
|
||||
Type componentType = ResolveType(typeName);
|
||||
if (componentType == null)
|
||||
return new { success = false, message = $"Could not resolve type '{typeName}'" };
|
||||
|
||||
string relativePath = @params["relativePath"]?.ToString() ?? "";
|
||||
|
||||
JToken keysToken = @params["keys"];
|
||||
if (keysToken == null)
|
||||
return new { success = false, message = "'keys' is required" };
|
||||
|
||||
var keyframes = ParseKeyframes(keysToken);
|
||||
if (keyframes == null || keyframes.Length == 0)
|
||||
return new { success = false, message = "Failed to parse keyframes. Use [{\"time\":0,\"value\":0},...] or [[0,0],[1,1],...]" };
|
||||
|
||||
AnimationCurve curve;
|
||||
var binding = EditorCurveBinding.FloatCurve(relativePath, componentType, propertyPath);
|
||||
|
||||
if (append)
|
||||
{
|
||||
curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve();
|
||||
foreach (var kf in keyframes)
|
||||
{
|
||||
curve.AddKey(kf);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
curve = new AnimationCurve(keyframes);
|
||||
}
|
||||
|
||||
// Use AnimationUtility.SetEditorCurve instead of clip.SetCurve to avoid
|
||||
// marking the clip as legacy — legacy clips cannot be used in Mecanim BlendTrees.
|
||||
Undo.RecordObject(clip, append ? "Add Animation Curve" : "Set Animation Curve");
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
string verb = append ? "Added" : "Set";
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"{verb} curve on '{propertyPath}' ({typeName}) with {keyframes.Length} keyframes",
|
||||
data = new
|
||||
{
|
||||
clipPath,
|
||||
propertyPath,
|
||||
type = typeName,
|
||||
keyframeCount = curve.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object SetVectorCurve(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
// Accept both 'property' and 'propertyPath' for consistency with add_curve/set_curve
|
||||
string property = @params["property"]?.ToString() ?? @params["propertyPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(property))
|
||||
return new { success = false, message = "'property' (or 'propertyPath') is required (e.g. 'localPosition', 'localEulerAngles', 'localScale')" };
|
||||
|
||||
string typeName = @params["type"]?.ToString() ?? "Transform";
|
||||
Type componentType = ResolveType(typeName);
|
||||
if (componentType == null)
|
||||
return new { success = false, message = $"Could not resolve type '{typeName}'" };
|
||||
|
||||
string relativePath = @params["relativePath"]?.ToString() ?? "";
|
||||
|
||||
JToken keysToken = @params["keys"];
|
||||
if (keysToken == null || keysToken is not JArray keysArray || keysArray.Count == 0)
|
||||
return new { success = false, message = "'keys' is required. Use [{\"time\":0,\"value\":[0,1,0]},...]" };
|
||||
|
||||
// Map property group to axis suffixes
|
||||
string[] suffixes;
|
||||
switch (property.ToLowerInvariant())
|
||||
{
|
||||
case "localposition":
|
||||
property = "localPosition";
|
||||
suffixes = new[] { ".x", ".y", ".z" };
|
||||
break;
|
||||
case "localeulerangles":
|
||||
property = "localEulerAngles";
|
||||
suffixes = new[] { ".x", ".y", ".z" };
|
||||
break;
|
||||
case "localscale":
|
||||
property = "localScale";
|
||||
suffixes = new[] { ".x", ".y", ".z" };
|
||||
break;
|
||||
default:
|
||||
suffixes = new[] { ".x", ".y", ".z" };
|
||||
break;
|
||||
}
|
||||
|
||||
var xKeys = new List<Keyframe>();
|
||||
var yKeys = new List<Keyframe>();
|
||||
var zKeys = new List<Keyframe>();
|
||||
|
||||
foreach (var item in keysArray)
|
||||
{
|
||||
if (item is not JObject keyObj)
|
||||
return new { success = false, message = "Each key must be an object with 'time' and 'value' (Vector3 array)" };
|
||||
|
||||
float time = keyObj["time"]?.ToObject<float>() ?? 0f;
|
||||
JToken valueToken = keyObj["value"];
|
||||
if (valueToken is not JArray valArray || valArray.Count < 3)
|
||||
return new { success = false, message = $"Key at time {time}: 'value' must be a 3-element array [x, y, z]" };
|
||||
|
||||
float vx = valArray[0].ToObject<float>();
|
||||
float vy = valArray[1].ToObject<float>();
|
||||
float vz = valArray[2].ToObject<float>();
|
||||
|
||||
xKeys.Add(new Keyframe(time, vx));
|
||||
yKeys.Add(new Keyframe(time, vy));
|
||||
zKeys.Add(new Keyframe(time, vz));
|
||||
}
|
||||
|
||||
// Use AnimationUtility.SetEditorCurve instead of clip.SetCurve to avoid
|
||||
// marking the clip as legacy — legacy clips cannot be used in Mecanim BlendTrees.
|
||||
Undo.RecordObject(clip, "Set Vector Curve");
|
||||
var bindingX = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[0]);
|
||||
var bindingY = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[1]);
|
||||
var bindingZ = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[2]);
|
||||
AnimationUtility.SetEditorCurve(clip, bindingX, new AnimationCurve(xKeys.ToArray()));
|
||||
AnimationUtility.SetEditorCurve(clip, bindingY, new AnimationCurve(yKeys.ToArray()));
|
||||
AnimationUtility.SetEditorCurve(clip, bindingZ, new AnimationCurve(zKeys.ToArray()));
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Set 3 curves on '{property}' ({typeName}) with {keysArray.Count} vector keyframes",
|
||||
data = new
|
||||
{
|
||||
clipPath,
|
||||
property,
|
||||
type = typeName,
|
||||
curves = new[] { property + suffixes[0], property + suffixes[1], property + suffixes[2] },
|
||||
keyframeCount = keysArray.Count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object Assign(JObject @params)
|
||||
{
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
// Try legacy Animation component first
|
||||
var legacyAnim = go.GetComponent<UnityEngine.Animation>();
|
||||
if (legacyAnim != null)
|
||||
{
|
||||
var wasLegacy = clip.legacy;
|
||||
SetupLegacyClip(clip);
|
||||
Undo.RecordObject(legacyAnim, "Assign Animation Clip");
|
||||
legacyAnim.clip = clip;
|
||||
legacyAnim.AddClip(clip, clip.name);
|
||||
legacyAnim.playAutomatically = true;
|
||||
EditorUtility.SetDirty(legacyAnim);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
// Warn about AnimationEvents if present — they require a MonoBehaviour receiver
|
||||
var events = AnimationUtility.GetAnimationEvents(clip);
|
||||
string warning = "";
|
||||
if (events != null && events.Length > 0)
|
||||
{
|
||||
var eventNames = new System.Collections.Generic.List<string>();
|
||||
foreach (var e in events)
|
||||
eventNames.Add(e.functionName);
|
||||
warning = $" Warning: This clip has {events.Length} AnimationEvent(s) ({string.Join(", ", eventNames)}). " +
|
||||
$"'{go.name}' must have a MonoBehaviour with matching method(s) to receive them, " +
|
||||
"otherwise Unity will log 'AnimationEvent has no receiver' errors.";
|
||||
}
|
||||
|
||||
if (!wasLegacy) warning += " Warning: clip was converted to legacy and will not be usable in Mecanim/BlendTrees.";
|
||||
|
||||
return new { success = true, message = $"Assigned clip '{clip.name}' to Animation component on '{go.name}'.{warning}" };
|
||||
}
|
||||
|
||||
// Add Animation component if no Animator or Animation exists
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
var wasLegacy = clip.legacy;
|
||||
SetupLegacyClip(clip);
|
||||
Undo.RecordObject(go, "Add Animation Component");
|
||||
legacyAnim = Undo.AddComponent<UnityEngine.Animation>(go);
|
||||
legacyAnim.clip = clip;
|
||||
legacyAnim.AddClip(clip, clip.name);
|
||||
legacyAnim.playAutomatically = true;
|
||||
EditorUtility.SetDirty(go);
|
||||
AssetDatabase.SaveAssets();
|
||||
var legacyWarning = !wasLegacy ? " Warning: clip was converted to legacy and will not be usable in Mecanim/BlendTrees." : "";
|
||||
return new { success = true, message = $"Added Animation component and assigned clip '{clip.name}' to '{go.name}'.{legacyWarning}" };
|
||||
}
|
||||
|
||||
// Has Animator - we can't programmatically assign clips to Animator states easily,
|
||||
// so report what the user should do
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"GameObject '{go.name}' has an Animator component. The clip '{clip.name}' is available at '{clipPath}'. " +
|
||||
"Assign it to an Animator Controller state via the Animator window or create an AnimatorOverrideController."
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetupLegacyClip(AnimationClip clip)
|
||||
{
|
||||
var so = new SerializedObject(clip);
|
||||
bool changed = false;
|
||||
|
||||
if (!clip.legacy)
|
||||
{
|
||||
var legacyProp = so.FindProperty("m_Legacy");
|
||||
if (legacyProp != null)
|
||||
{
|
||||
legacyProp.boolValue = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
||||
if (settings.loopTime)
|
||||
{
|
||||
var wrapProp = so.FindProperty("m_WrapMode");
|
||||
if (wrapProp != null && wrapProp.intValue != (int)WrapMode.Loop)
|
||||
{
|
||||
wrapProp.intValue = (int)WrapMode.Loop;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private static Keyframe[] ParseKeyframes(JToken keysToken)
|
||||
{
|
||||
if (keysToken is JArray array && array.Count > 0)
|
||||
{
|
||||
var keyframes = new List<Keyframe>();
|
||||
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is JArray pair && pair.Count >= 2)
|
||||
{
|
||||
// Shorthand: [time, value]
|
||||
float time = pair[0].ToObject<float>();
|
||||
float value = pair[1].ToObject<float>();
|
||||
keyframes.Add(new Keyframe(time, value));
|
||||
}
|
||||
else if (item is JObject obj)
|
||||
{
|
||||
// Full form: {"time":0, "value":0, "inTangent":0, "outTangent":0}
|
||||
float time = obj["time"]?.ToObject<float>() ?? 0f;
|
||||
float value = obj["value"]?.ToObject<float>() ?? 0f;
|
||||
|
||||
var kf = new Keyframe(time, value);
|
||||
if (obj["inTangent"] != null)
|
||||
kf.inTangent = obj["inTangent"].ToObject<float>();
|
||||
if (obj["outTangent"] != null)
|
||||
kf.outTangent = obj["outTangent"].ToObject<float>();
|
||||
if (obj["inWeight"] != null)
|
||||
kf.inWeight = obj["inWeight"].ToObject<float>();
|
||||
if (obj["outWeight"] != null)
|
||||
kf.outWeight = obj["outWeight"].ToObject<float>();
|
||||
|
||||
keyframes.Add(kf);
|
||||
}
|
||||
}
|
||||
|
||||
return keyframes.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Type ResolveType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
return typeof(Transform);
|
||||
|
||||
// Try common Unity types
|
||||
Type type = Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule");
|
||||
if (type != null) return type;
|
||||
|
||||
type = Type.GetType($"UnityEngine.{typeName}, UnityEngine.AnimationModule");
|
||||
if (type != null) return type;
|
||||
|
||||
type = Type.GetType($"UnityEngine.{typeName}, UnityEngine");
|
||||
if (type != null) return type;
|
||||
|
||||
// Try fully qualified
|
||||
type = Type.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
|
||||
// Fallback: search all loaded assemblies
|
||||
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
|
||||
{
|
||||
type = assembly.GetType(typeName);
|
||||
if (type != null) return type;
|
||||
|
||||
type = assembly.GetType($"UnityEngine.{typeName}");
|
||||
if (type != null) return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static object AddEvent(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
float time = @params["time"]?.ToObject<float>() ?? 0f;
|
||||
string functionName = @params["functionName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(functionName))
|
||||
return new { success = false, message = "'functionName' is required" };
|
||||
|
||||
var animEvent = new AnimationEvent
|
||||
{
|
||||
time = time,
|
||||
functionName = functionName,
|
||||
stringParameter = @params["stringParameter"]?.ToString() ?? "",
|
||||
floatParameter = @params["floatParameter"]?.ToObject<float>() ?? 0f,
|
||||
intParameter = @params["intParameter"]?.ToObject<int>() ?? 0
|
||||
};
|
||||
|
||||
var events = AnimationUtility.GetAnimationEvents(clip).ToList();
|
||||
events.Add(animEvent);
|
||||
AnimationUtility.SetAnimationEvents(clip, events.ToArray());
|
||||
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added event '{functionName}' at time {time} to '{clipPath}'",
|
||||
data = new
|
||||
{
|
||||
clipPath,
|
||||
time,
|
||||
functionName,
|
||||
stringParameter = animEvent.stringParameter,
|
||||
floatParameter = animEvent.floatParameter,
|
||||
intParameter = animEvent.intParameter
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object RemoveEvent(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
var events = AnimationUtility.GetAnimationEvents(clip).ToList();
|
||||
int originalCount = events.Count;
|
||||
|
||||
int? eventIndex = @params["eventIndex"]?.ToObject<int?>();
|
||||
if (eventIndex.HasValue)
|
||||
{
|
||||
if (eventIndex.Value < 0 || eventIndex.Value >= events.Count)
|
||||
return new { success = false, message = $"Event index {eventIndex.Value} out of range (0-{events.Count - 1})" };
|
||||
|
||||
events.RemoveAt(eventIndex.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
string functionName = @params["functionName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(functionName))
|
||||
return new { success = false, message = "Either 'eventIndex' or 'functionName' is required" };
|
||||
|
||||
float? timeFilter = @params["time"]?.ToObject<float?>();
|
||||
events.RemoveAll(e =>
|
||||
{
|
||||
bool matchesFunction = e.functionName == functionName;
|
||||
bool matchesTime = !timeFilter.HasValue || Mathf.Approximately(e.time, timeFilter.Value);
|
||||
return matchesFunction && matchesTime;
|
||||
});
|
||||
}
|
||||
|
||||
int removedCount = originalCount - events.Count;
|
||||
if (removedCount == 0)
|
||||
return new { success = false, message = "No matching events found to remove" };
|
||||
|
||||
AnimationUtility.SetAnimationEvents(clip, events.ToArray());
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Removed {removedCount} event(s) from '{clipPath}'",
|
||||
data = new
|
||||
{
|
||||
clipPath,
|
||||
removedCount,
|
||||
remainingCount = events.Count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void CreateFoldersRecursive(string folderPath)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(folderPath))
|
||||
return;
|
||||
|
||||
string parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(parent) && parent != "Assets" && !AssetDatabase.IsValidFolder(parent))
|
||||
{
|
||||
CreateFoldersRecursive(parent);
|
||||
}
|
||||
|
||||
string folderName = Path.GetFileName(folderPath);
|
||||
if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(folderName))
|
||||
{
|
||||
AssetDatabase.CreateFolder(parent, folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7513801696d4b16b906561009481548
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,401 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class ClipPresets
|
||||
{
|
||||
private static readonly string[] ValidPresets = { "bounce", "rotate", "pulse", "fade", "shake", "hover", "spin", "sway", "bob", "wiggle", "blink", "slide_in", "elastic", "grow", "shrink" };
|
||||
|
||||
public static object CreatePreset(JObject @params)
|
||||
{
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required (e.g. 'Assets/Animations/Bounce.anim')" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
if (!clipPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
clipPath += ".anim";
|
||||
|
||||
string preset = @params["preset"]?.ToString()?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(preset))
|
||||
return new { success = false, message = $"'preset' is required. Valid: {string.Join(", ", ValidPresets)}" };
|
||||
|
||||
float duration = @params["duration"]?.ToObject<float>() ?? 1f;
|
||||
float amplitude = @params["amplitude"]?.ToObject<float>() ?? 1f;
|
||||
bool loop = @params["loop"]?.ToObject<bool>() ?? true;
|
||||
|
||||
// Resolve position offset from target GameObject or explicit offset parameter.
|
||||
// localPosition rather than absolute origin, preventing objects from jumping to (0,0,0).
|
||||
Vector3 offset = Vector3.zero;
|
||||
var targetToken = @params["target"];
|
||||
if (targetToken != null && targetToken.Type != JTokenType.Null)
|
||||
{
|
||||
string searchMethod = @params["searchMethod"]?.ToString();
|
||||
var go = ObjectResolver.ResolveGameObject(targetToken, searchMethod);
|
||||
if (go != null)
|
||||
offset = go.transform.localPosition;
|
||||
}
|
||||
var offsetToken = @params["offset"];
|
||||
if (offsetToken is JArray offsetArray && offsetArray.Count >= 3)
|
||||
{
|
||||
offset = new Vector3(
|
||||
offsetArray[0].ToObject<float>(),
|
||||
offsetArray[1].ToObject<float>(),
|
||||
offsetArray[2].ToObject<float>()
|
||||
);
|
||||
}
|
||||
|
||||
string dir = Path.GetDirectoryName(clipPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
|
||||
CreateFoldersRecursive(dir);
|
||||
|
||||
var existing = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (existing != null)
|
||||
return new { success = false, message = $"AnimationClip already exists at '{clipPath}'. Delete it first or use a different path." };
|
||||
|
||||
var clip = new AnimationClip();
|
||||
clip.name = Path.GetFileNameWithoutExtension(clipPath);
|
||||
clip.frameRate = 60f;
|
||||
|
||||
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
||||
settings.loopTime = loop;
|
||||
settings.stopTime = duration;
|
||||
AnimationUtility.SetAnimationClipSettings(clip, settings);
|
||||
|
||||
switch (preset)
|
||||
{
|
||||
case "bounce":
|
||||
ApplyBounce(clip, duration, amplitude, offset);
|
||||
break;
|
||||
case "rotate":
|
||||
ApplyRotate(clip, duration, amplitude);
|
||||
break;
|
||||
case "pulse":
|
||||
ApplyPulse(clip, duration, amplitude);
|
||||
break;
|
||||
case "fade":
|
||||
ApplyFade(clip, duration);
|
||||
break;
|
||||
case "shake":
|
||||
ApplyShake(clip, duration, amplitude, offset);
|
||||
break;
|
||||
case "hover":
|
||||
ApplyHover(clip, duration, amplitude, offset);
|
||||
break;
|
||||
case "spin":
|
||||
ApplySpin(clip, duration, amplitude);
|
||||
break;
|
||||
case "sway":
|
||||
ApplySway(clip, duration, amplitude);
|
||||
break;
|
||||
case "bob":
|
||||
ApplyBob(clip, duration, amplitude, offset);
|
||||
break;
|
||||
case "wiggle":
|
||||
ApplyWiggle(clip, duration, amplitude);
|
||||
break;
|
||||
case "blink":
|
||||
ApplyBlink(clip, duration);
|
||||
break;
|
||||
case "slide_in":
|
||||
ApplySlideIn(clip, duration, amplitude, offset);
|
||||
break;
|
||||
case "elastic":
|
||||
ApplyElastic(clip, duration, amplitude);
|
||||
break;
|
||||
case "grow":
|
||||
ApplyGrow(clip, duration, amplitude);
|
||||
break;
|
||||
case "shrink":
|
||||
ApplyShrink(clip, duration, amplitude);
|
||||
break;
|
||||
default:
|
||||
return new { success = false, message = $"Unknown preset '{preset}'. Valid: {string.Join(", ", ValidPresets)}" };
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(clip, clipPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created '{preset}' preset clip at '{clipPath}'" + (offset != Vector3.zero ? $" (offset: {offset})" : ""),
|
||||
data = new
|
||||
{
|
||||
path = clipPath,
|
||||
name = clip.name,
|
||||
preset,
|
||||
duration,
|
||||
amplitude,
|
||||
isLooping = loop,
|
||||
offset = new { x = offset.x, y = offset.y, z = offset.z },
|
||||
curveCount = AnimationUtility.GetCurveBindings(clip).Length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyBounce(AnimationClip clip, float duration, float amplitude, Vector3 offset)
|
||||
{
|
||||
// localPosition.y sine wave oscillation, offset by target's current position
|
||||
float half = duration * 0.5f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, offset.y),
|
||||
new Keyframe(half * 0.5f, offset.y + amplitude),
|
||||
new Keyframe(half, offset.y),
|
||||
new Keyframe(half + half * 0.5f, offset.y + amplitude),
|
||||
new Keyframe(duration, offset.y)
|
||||
);
|
||||
SetTransformCurve(clip, "localPosition.y", curve);
|
||||
}
|
||||
|
||||
private static void ApplyRotate(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localEulerAngles.y full 360 rotation (amplitude acts as multiplier)
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(duration, 360f * amplitude)
|
||||
);
|
||||
// Linear tangents for smooth rotation
|
||||
var keys = curve.keys;
|
||||
keys[0].outTangent = 360f * amplitude / duration;
|
||||
keys[1].inTangent = 360f * amplitude / duration;
|
||||
curve.keys = keys;
|
||||
SetTransformCurve(clip, "localEulerAngles.y", curve);
|
||||
}
|
||||
|
||||
private static void ApplyPulse(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localScale uniform scale up/down
|
||||
float peak = 1f + amplitude * 0.5f;
|
||||
float half = duration * 0.5f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(half, peak),
|
||||
new Keyframe(duration, 1f)
|
||||
);
|
||||
SetTransformCurve(clip, "localScale.x", curve);
|
||||
SetTransformCurve(clip, "localScale.y", curve);
|
||||
SetTransformCurve(clip, "localScale.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplyFade(AnimationClip clip, float duration)
|
||||
{
|
||||
// CanvasGroup alpha 1 -> 0
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(duration, 0f)
|
||||
);
|
||||
var binding = EditorCurveBinding.FloatCurve("", typeof(CanvasGroup), "m_Alpha");
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
}
|
||||
|
||||
private static void ApplyShake(AnimationClip clip, float duration, float amplitude, Vector3 offset)
|
||||
{
|
||||
// localPosition.x/z oscillation simulating shake, centered on target's current position
|
||||
int steps = 8;
|
||||
float stepTime = duration / steps;
|
||||
var xKeys = new Keyframe[steps + 1];
|
||||
var zKeys = new Keyframe[steps + 1];
|
||||
|
||||
for (int i = 0; i <= steps; i++)
|
||||
{
|
||||
float t = i * stepTime;
|
||||
float decay = 1f - (float)i / steps;
|
||||
// Alternating direction with decay
|
||||
float sign = (i % 2 == 0) ? 1f : -1f;
|
||||
xKeys[i] = new Keyframe(t, offset.x + sign * amplitude * decay);
|
||||
zKeys[i] = new Keyframe(t, offset.z - sign * amplitude * 0.5f * decay);
|
||||
}
|
||||
|
||||
// End at offset position
|
||||
xKeys[steps] = new Keyframe(duration, offset.x);
|
||||
zKeys[steps] = new Keyframe(duration, offset.z);
|
||||
|
||||
SetTransformCurve(clip, "localPosition.x", new AnimationCurve(xKeys));
|
||||
SetTransformCurve(clip, "localPosition.z", new AnimationCurve(zKeys));
|
||||
}
|
||||
|
||||
private static void ApplyHover(AnimationClip clip, float duration, float amplitude, Vector3 offset)
|
||||
{
|
||||
// localPosition.y gentle sine wave, offset by target's current position
|
||||
float q = duration * 0.25f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, offset.y),
|
||||
new Keyframe(q, offset.y + amplitude * 0.5f),
|
||||
new Keyframe(q * 2f, offset.y),
|
||||
new Keyframe(q * 3f, offset.y - amplitude * 0.5f),
|
||||
new Keyframe(duration, offset.y)
|
||||
);
|
||||
SetTransformCurve(clip, "localPosition.y", curve);
|
||||
}
|
||||
|
||||
private static void ApplySpin(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localEulerAngles.z continuous rotation
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(duration, 360f * amplitude)
|
||||
);
|
||||
var keys = curve.keys;
|
||||
keys[0].outTangent = 360f * amplitude / duration;
|
||||
keys[1].inTangent = 360f * amplitude / duration;
|
||||
curve.keys = keys;
|
||||
SetTransformCurve(clip, "localEulerAngles.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplySway(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localEulerAngles.z gentle side-to-side rotation (sine wave)
|
||||
float q = duration * 0.25f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(q, amplitude),
|
||||
new Keyframe(q * 2f, 0f),
|
||||
new Keyframe(q * 3f, -amplitude),
|
||||
new Keyframe(duration, 0f)
|
||||
);
|
||||
SetTransformCurve(clip, "localEulerAngles.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplyBob(AnimationClip clip, float duration, float amplitude, Vector3 offset)
|
||||
{
|
||||
// localPosition.z gentle forward/back movement, offset by target's current position
|
||||
float q = duration * 0.25f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, offset.z),
|
||||
new Keyframe(q, offset.z + amplitude * 0.5f),
|
||||
new Keyframe(q * 2f, offset.z),
|
||||
new Keyframe(q * 3f, offset.z - amplitude * 0.5f),
|
||||
new Keyframe(duration, offset.z)
|
||||
);
|
||||
SetTransformCurve(clip, "localPosition.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplyWiggle(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localEulerAngles.z rapid oscillation (similar to shake but rotation)
|
||||
int steps = 8;
|
||||
float stepTime = duration / steps;
|
||||
var keys = new Keyframe[steps + 1];
|
||||
|
||||
for (int i = 0; i <= steps; i++)
|
||||
{
|
||||
float t = i * stepTime;
|
||||
float decay = 1f - (float)i / steps;
|
||||
float sign = (i % 2 == 0) ? 1f : -1f;
|
||||
keys[i] = new Keyframe(t, sign * amplitude * decay);
|
||||
}
|
||||
|
||||
keys[steps] = new Keyframe(duration, 0f);
|
||||
SetTransformCurve(clip, "localEulerAngles.z", new AnimationCurve(keys));
|
||||
}
|
||||
|
||||
private static void ApplyBlink(AnimationClip clip, float duration)
|
||||
{
|
||||
// localScale uniform scale to near-zero and back
|
||||
float mid = duration * 0.5f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(mid, 0.05f),
|
||||
new Keyframe(duration, 1f)
|
||||
);
|
||||
SetTransformCurve(clip, "localScale.x", curve);
|
||||
SetTransformCurve(clip, "localScale.y", curve);
|
||||
SetTransformCurve(clip, "localScale.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplySlideIn(AnimationClip clip, float duration, float amplitude, Vector3 offset)
|
||||
{
|
||||
// localPosition.x slide from offset-amplitude to offset (linear)
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, offset.x - amplitude),
|
||||
new Keyframe(duration, offset.x)
|
||||
);
|
||||
// Set linear tangents for smooth slide
|
||||
var keys = curve.keys;
|
||||
keys[0].outTangent = amplitude / duration;
|
||||
keys[1].inTangent = amplitude / duration;
|
||||
curve.keys = keys;
|
||||
SetTransformCurve(clip, "localPosition.x", curve);
|
||||
}
|
||||
|
||||
private static void ApplyElastic(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localScale uniform with overshoot effect
|
||||
float third = duration / 3f;
|
||||
float peak = 1f + amplitude * 1.2f;
|
||||
float settle = 1f + amplitude * 0.8f;
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(third, peak),
|
||||
new Keyframe(third * 2f, settle),
|
||||
new Keyframe(duration, 1f)
|
||||
);
|
||||
SetTransformCurve(clip, "localScale.x", curve);
|
||||
SetTransformCurve(clip, "localScale.y", curve);
|
||||
SetTransformCurve(clip, "localScale.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplyGrow(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localScale uniform from a reduced value up to 1.0
|
||||
float clamped = Mathf.Max(0f, amplitude);
|
||||
float start = Mathf.Clamp01(1f - clamped);
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, start),
|
||||
new Keyframe(duration, 1f)
|
||||
);
|
||||
SetTransformCurve(clip, "localScale.x", curve);
|
||||
SetTransformCurve(clip, "localScale.y", curve);
|
||||
SetTransformCurve(clip, "localScale.z", curve);
|
||||
}
|
||||
|
||||
private static void ApplyShrink(AnimationClip clip, float duration, float amplitude)
|
||||
{
|
||||
// localScale uniform from 1.0 down to a reduced value
|
||||
float clamped = Mathf.Max(0f, amplitude);
|
||||
float end = Mathf.Clamp01(1f - clamped);
|
||||
var curve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(duration, end)
|
||||
);
|
||||
SetTransformCurve(clip, "localScale.x", curve);
|
||||
SetTransformCurve(clip, "localScale.y", curve);
|
||||
SetTransformCurve(clip, "localScale.z", curve);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an animation curve on a Transform property using AnimationUtility.SetEditorCurve
|
||||
/// instead of clip.SetCurve to avoid marking the clip as legacy. Legacy clips cannot be
|
||||
/// used with Mecanim AnimatorControllers, and legacy Animation components take control of
|
||||
/// the entire Vector3 property (zeroing non-animated axes).
|
||||
/// </summary>
|
||||
private static void SetTransformCurve(AnimationClip clip, string propertyName, AnimationCurve curve)
|
||||
{
|
||||
var binding = EditorCurveBinding.FloatCurve("", typeof(Transform), propertyName);
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
}
|
||||
|
||||
private static void CreateFoldersRecursive(string folderPath)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(folderPath))
|
||||
return;
|
||||
|
||||
string parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(parent) && parent != "Assets" && !AssetDatabase.IsValidFolder(parent))
|
||||
CreateFoldersRecursive(parent);
|
||||
|
||||
string folderName = Path.GetFileName(folderPath);
|
||||
if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(folderName))
|
||||
AssetDatabase.CreateFolder(parent, folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5e91d58c0a24e6db187f2a3c6840e29
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,255 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class ControllerBlendTrees
|
||||
{
|
||||
public static object CreateBlendTree1D(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
string blendParameter = @params["blendParameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(blendParameter))
|
||||
return new { success = false, message = "'blendParameter' is required" };
|
||||
|
||||
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
|
||||
|
||||
var layers = controller.layers;
|
||||
if (layerIndex < 0 || layerIndex >= layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex} out of range (0-{layers.Length - 1})" };
|
||||
|
||||
var stateMachine = layers[layerIndex].stateMachine;
|
||||
|
||||
Undo.RecordObject(controller, "Create Blend Tree 1D");
|
||||
var state = stateMachine.AddState(stateName);
|
||||
var blendTree = new BlendTree
|
||||
{
|
||||
name = stateName,
|
||||
blendType = BlendTreeType.Simple1D,
|
||||
blendParameter = blendParameter,
|
||||
hideFlags = HideFlags.HideInHierarchy
|
||||
};
|
||||
|
||||
AssetDatabase.AddObjectToAsset(blendTree, controller);
|
||||
state.motion = blendTree;
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created 1D blend tree state '{stateName}' in '{controllerPath}'",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
stateName,
|
||||
layerIndex,
|
||||
blendParameter,
|
||||
blendType = "Simple1D"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object CreateBlendTree2D(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
string blendParameterX = @params["blendParameterX"]?.ToString();
|
||||
string blendParameterY = @params["blendParameterY"]?.ToString();
|
||||
if (string.IsNullOrEmpty(blendParameterX) || string.IsNullOrEmpty(blendParameterY))
|
||||
return new { success = false, message = "'blendParameterX' and 'blendParameterY' are required" };
|
||||
|
||||
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
|
||||
string blendTypeStr = @params["blendType"]?.ToString()?.ToLowerInvariant() ?? "simpledirectional2d";
|
||||
|
||||
BlendTreeType blendType = blendTypeStr switch
|
||||
{
|
||||
"freeformdirectional2d" => BlendTreeType.FreeformDirectional2D,
|
||||
"freeformcartesian2d" => BlendTreeType.FreeformCartesian2D,
|
||||
_ => BlendTreeType.SimpleDirectional2D
|
||||
};
|
||||
|
||||
var layers = controller.layers;
|
||||
if (layerIndex < 0 || layerIndex >= layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex} out of range (0-{layers.Length - 1})" };
|
||||
|
||||
var stateMachine = layers[layerIndex].stateMachine;
|
||||
|
||||
Undo.RecordObject(controller, "Create Blend Tree 2D");
|
||||
var state = stateMachine.AddState(stateName);
|
||||
var blendTree = new BlendTree
|
||||
{
|
||||
name = stateName,
|
||||
blendType = blendType,
|
||||
blendParameter = blendParameterX,
|
||||
blendParameterY = blendParameterY,
|
||||
hideFlags = HideFlags.HideInHierarchy
|
||||
};
|
||||
|
||||
AssetDatabase.AddObjectToAsset(blendTree, controller);
|
||||
state.motion = blendTree;
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created 2D blend tree state '{stateName}' in '{controllerPath}'",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
stateName,
|
||||
layerIndex,
|
||||
blendParameterX,
|
||||
blendParameterY,
|
||||
blendType = blendType.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AddBlendTreeChild(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(clipPath))
|
||||
return new { success = false, message = "'clipPath' is required" };
|
||||
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip == null)
|
||||
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
||||
|
||||
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
|
||||
|
||||
var layers = controller.layers;
|
||||
if (layerIndex < 0 || layerIndex >= layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex} out of range (0-{layers.Length - 1})" };
|
||||
|
||||
var stateMachine = layers[layerIndex].stateMachine;
|
||||
AnimatorState state = null;
|
||||
foreach (var s in stateMachine.states)
|
||||
{
|
||||
if (s.state.name == stateName)
|
||||
{
|
||||
state = s.state;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == null)
|
||||
return new { success = false, message = $"State '{stateName}' not found in layer {layerIndex}" };
|
||||
|
||||
if (!(state.motion is BlendTree blendTree))
|
||||
return new { success = false, message = $"State '{stateName}' does not have a BlendTree motion" };
|
||||
|
||||
Undo.RecordObject(blendTree, "Add Blend Tree Child");
|
||||
|
||||
if (blendTree.blendType == BlendTreeType.Simple1D)
|
||||
{
|
||||
float? threshold = @params["threshold"]?.ToObject<float?>();
|
||||
if (!threshold.HasValue)
|
||||
return new { success = false, message = "'threshold' is required for 1D blend trees" };
|
||||
|
||||
blendTree.AddChild(clip, threshold.Value);
|
||||
|
||||
EditorUtility.SetDirty(blendTree);
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added clip '{clip.name}' to blend tree '{stateName}' at threshold {threshold.Value}",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
stateName,
|
||||
clipPath,
|
||||
threshold = threshold.Value,
|
||||
childCount = blendTree.children.Length
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
JToken positionToken = @params["position"];
|
||||
if (positionToken == null || !(positionToken is JArray posArray) || posArray.Count < 2)
|
||||
return new { success = false, message = "'position' is required for 2D blend trees as [x, y]" };
|
||||
|
||||
float posX = posArray[0].ToObject<float>();
|
||||
float posY = posArray[1].ToObject<float>();
|
||||
Vector2 position = new Vector2(posX, posY);
|
||||
|
||||
blendTree.AddChild(clip, position);
|
||||
|
||||
EditorUtility.SetDirty(blendTree);
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added clip '{clip.name}' to blend tree '{stateName}' at position ({posX}, {posY})",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
stateName,
|
||||
clipPath,
|
||||
position = new { x = posX, y = posY },
|
||||
childCount = blendTree.children.Length
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8d4f0a2e53c5b7f9a6e1d4c8f0b2a5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,458 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class ControllerCreate
|
||||
{
|
||||
public static object Create(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required (e.g. 'Assets/Animations/Player.controller')" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
if (!controllerPath.EndsWith(".controller", StringComparison.OrdinalIgnoreCase))
|
||||
controllerPath += ".controller";
|
||||
|
||||
string dir = Path.GetDirectoryName(controllerPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
|
||||
CreateFoldersRecursive(dir);
|
||||
|
||||
var existing = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (existing != null)
|
||||
return new { success = false, message = $"AnimatorController already exists at '{controllerPath}'. Delete it first or use a different path." };
|
||||
|
||||
var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created AnimatorController at '{controllerPath}'",
|
||||
data = new
|
||||
{
|
||||
path = controllerPath,
|
||||
name = controller.name,
|
||||
layerCount = controller.layers.Length,
|
||||
parameterCount = controller.parameters.Length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AddState(JObject @params)
|
||||
{
|
||||
var controller = LoadController(@params);
|
||||
if (controller == null)
|
||||
return ControllerNotFoundError(@params);
|
||||
|
||||
string stateName = @params["stateName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(stateName))
|
||||
return new { success = false, message = "'stateName' is required" };
|
||||
|
||||
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
|
||||
if (layerIndex < 0 || layerIndex >= controller.layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex} out of range (controller has {controller.layers.Length} layers)" };
|
||||
|
||||
var rootStateMachine = controller.layers[layerIndex].stateMachine;
|
||||
|
||||
// Check for duplicate state name
|
||||
foreach (var existingState in rootStateMachine.states)
|
||||
{
|
||||
if (existingState.state.name == stateName)
|
||||
return new { success = false, message = $"State '{stateName}' already exists in layer {layerIndex}" };
|
||||
}
|
||||
|
||||
var state = rootStateMachine.AddState(stateName);
|
||||
|
||||
// Optionally assign a clip
|
||||
string clipPath = @params["clipPath"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(clipPath))
|
||||
{
|
||||
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
||||
if (clipPath != null)
|
||||
{
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
||||
if (clip != null)
|
||||
state.motion = clip;
|
||||
}
|
||||
}
|
||||
|
||||
float speed = @params["speed"]?.ToObject<float>() ?? 1f;
|
||||
state.speed = speed;
|
||||
|
||||
bool isDefault = @params["isDefault"]?.ToObject<bool>() ?? false;
|
||||
if (isDefault)
|
||||
rootStateMachine.defaultState = state;
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added state '{stateName}' to layer {layerIndex}",
|
||||
data = new
|
||||
{
|
||||
stateName,
|
||||
layerIndex,
|
||||
hasMotion = state.motion != null,
|
||||
speed = state.speed,
|
||||
isDefault
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AddTransition(JObject @params)
|
||||
{
|
||||
var controller = LoadController(@params);
|
||||
if (controller == null)
|
||||
return ControllerNotFoundError(@params);
|
||||
|
||||
string fromStateName = @params["fromState"]?.ToString();
|
||||
string toStateName = @params["toState"]?.ToString();
|
||||
if (string.IsNullOrEmpty(fromStateName) || string.IsNullOrEmpty(toStateName))
|
||||
return new { success = false, message = "'fromState' and 'toState' are required" };
|
||||
|
||||
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
|
||||
if (layerIndex < 0 || layerIndex >= controller.layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex} out of range" };
|
||||
|
||||
var rootStateMachine = controller.layers[layerIndex].stateMachine;
|
||||
|
||||
// Check for AnyState as source
|
||||
bool isAnyState = string.Equals(fromStateName, "AnyState", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(fromStateName, "Any", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(fromStateName, "Any State", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
AnimatorState toState = null;
|
||||
foreach (var cs in rootStateMachine.states)
|
||||
{
|
||||
if (cs.state.name == toStateName) toState = cs.state;
|
||||
}
|
||||
|
||||
if (toState == null)
|
||||
return new { success = false, message = $"State '{toStateName}' not found in layer {layerIndex}" };
|
||||
|
||||
AnimatorStateTransition transition;
|
||||
if (isAnyState)
|
||||
{
|
||||
transition = rootStateMachine.AddAnyStateTransition(toState);
|
||||
fromStateName = "AnyState";
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimatorState fromState = null;
|
||||
foreach (var cs in rootStateMachine.states)
|
||||
{
|
||||
if (cs.state.name == fromStateName) fromState = cs.state;
|
||||
}
|
||||
|
||||
if (fromState == null)
|
||||
return new { success = false, message = $"State '{fromStateName}' not found in layer {layerIndex}" };
|
||||
|
||||
transition = fromState.AddTransition(toState);
|
||||
}
|
||||
|
||||
bool hasExitTime = @params["hasExitTime"]?.ToObject<bool>() ?? true;
|
||||
transition.hasExitTime = hasExitTime;
|
||||
|
||||
float duration = @params["duration"]?.ToObject<float>() ?? 0.25f;
|
||||
transition.duration = duration;
|
||||
|
||||
float exitTime = @params["exitTime"]?.ToObject<float>() ?? 0.75f;
|
||||
transition.exitTime = exitTime;
|
||||
|
||||
// Add conditions
|
||||
JToken conditionsToken = @params["conditions"];
|
||||
int conditionCount = 0;
|
||||
if (conditionsToken is JArray conditionsArray)
|
||||
{
|
||||
foreach (var condItem in conditionsArray)
|
||||
{
|
||||
if (condItem is not JObject condObj) continue;
|
||||
|
||||
string paramName = condObj["parameter"]?.ToString();
|
||||
if (string.IsNullOrEmpty(paramName)) continue;
|
||||
|
||||
string modeStr = condObj["mode"]?.ToString()?.ToLowerInvariant() ?? "greater";
|
||||
float threshold = condObj["threshold"]?.ToObject<float>() ?? 0f;
|
||||
|
||||
AnimatorConditionMode mode;
|
||||
switch (modeStr)
|
||||
{
|
||||
case "greater": mode = AnimatorConditionMode.Greater; break;
|
||||
case "less": mode = AnimatorConditionMode.Less; break;
|
||||
case "equals": mode = AnimatorConditionMode.Equals; break;
|
||||
case "notequal":
|
||||
case "not_equal": mode = AnimatorConditionMode.NotEqual; break;
|
||||
case "if":
|
||||
case "true": mode = AnimatorConditionMode.If; break;
|
||||
case "ifnot":
|
||||
case "if_not":
|
||||
case "false": mode = AnimatorConditionMode.IfNot; break;
|
||||
default: mode = AnimatorConditionMode.Greater; break;
|
||||
}
|
||||
|
||||
transition.AddCondition(mode, threshold, paramName);
|
||||
conditionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added transition from '{fromStateName}' to '{toStateName}' with {conditionCount} conditions",
|
||||
data = new
|
||||
{
|
||||
fromState = fromStateName,
|
||||
toState = toStateName,
|
||||
hasExitTime,
|
||||
duration,
|
||||
conditionCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AddParameter(JObject @params)
|
||||
{
|
||||
var controller = LoadController(@params);
|
||||
if (controller == null)
|
||||
return ControllerNotFoundError(@params);
|
||||
|
||||
string paramName = @params["parameterName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(paramName))
|
||||
return new { success = false, message = "'parameterName' is required" };
|
||||
|
||||
string typeStr = @params["parameterType"]?.ToString()?.ToLowerInvariant() ?? "float";
|
||||
|
||||
AnimatorControllerParameterType paramType;
|
||||
switch (typeStr)
|
||||
{
|
||||
case "float": paramType = AnimatorControllerParameterType.Float; break;
|
||||
case "int":
|
||||
case "integer": paramType = AnimatorControllerParameterType.Int; break;
|
||||
case "bool":
|
||||
case "boolean": paramType = AnimatorControllerParameterType.Bool; break;
|
||||
case "trigger": paramType = AnimatorControllerParameterType.Trigger; break;
|
||||
default:
|
||||
return new { success = false, message = $"Unknown parameter type '{typeStr}'. Valid: float, int, bool, trigger" };
|
||||
}
|
||||
|
||||
// Check for duplicate
|
||||
foreach (var existing in controller.parameters)
|
||||
{
|
||||
if (existing.name == paramName)
|
||||
return new { success = false, message = $"Parameter '{paramName}' already exists" };
|
||||
}
|
||||
|
||||
controller.AddParameter(paramName, paramType);
|
||||
|
||||
// Set default value if provided
|
||||
JToken defaultValue = @params["defaultValue"];
|
||||
if (defaultValue != null)
|
||||
{
|
||||
var allParams = controller.parameters;
|
||||
var addedParam = allParams[allParams.Length - 1];
|
||||
|
||||
switch (paramType)
|
||||
{
|
||||
case AnimatorControllerParameterType.Float:
|
||||
addedParam.defaultFloat = defaultValue.ToObject<float>();
|
||||
break;
|
||||
case AnimatorControllerParameterType.Int:
|
||||
addedParam.defaultInt = defaultValue.ToObject<int>();
|
||||
break;
|
||||
case AnimatorControllerParameterType.Bool:
|
||||
addedParam.defaultBool = defaultValue.ToObject<bool>();
|
||||
break;
|
||||
}
|
||||
|
||||
controller.parameters = allParams;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added {typeStr} parameter '{paramName}'",
|
||||
data = new
|
||||
{
|
||||
parameterName = paramName,
|
||||
parameterType = typeStr,
|
||||
totalParameters = controller.parameters.Length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object GetInfo(JObject @params)
|
||||
{
|
||||
var controller = LoadController(@params);
|
||||
if (controller == null)
|
||||
return ControllerNotFoundError(@params);
|
||||
|
||||
var layers = new List<object>();
|
||||
for (int i = 0; i < controller.layers.Length; i++)
|
||||
{
|
||||
var layer = controller.layers[i];
|
||||
var states = new List<object>();
|
||||
foreach (var cs in layer.stateMachine.states)
|
||||
{
|
||||
var transitions = new List<object>();
|
||||
foreach (var t in cs.state.transitions)
|
||||
{
|
||||
var conditions = new List<object>();
|
||||
foreach (var c in t.conditions)
|
||||
{
|
||||
conditions.Add(new
|
||||
{
|
||||
parameter = c.parameter,
|
||||
mode = c.mode.ToString(),
|
||||
threshold = c.threshold
|
||||
});
|
||||
}
|
||||
|
||||
transitions.Add(new
|
||||
{
|
||||
destinationState = t.destinationState?.name,
|
||||
hasExitTime = t.hasExitTime,
|
||||
exitTime = t.exitTime,
|
||||
duration = t.duration,
|
||||
conditionCount = t.conditions.Length,
|
||||
conditions
|
||||
});
|
||||
}
|
||||
|
||||
states.Add(new
|
||||
{
|
||||
name = cs.state.name,
|
||||
speed = cs.state.speed,
|
||||
hasMotion = cs.state.motion != null,
|
||||
motionName = cs.state.motion?.name,
|
||||
isDefault = layer.stateMachine.defaultState == cs.state,
|
||||
transitionCount = cs.state.transitions.Length,
|
||||
transitions
|
||||
});
|
||||
}
|
||||
|
||||
layers.Add(new
|
||||
{
|
||||
index = i,
|
||||
name = layer.name,
|
||||
stateCount = layer.stateMachine.states.Length,
|
||||
states
|
||||
});
|
||||
}
|
||||
|
||||
var parameters = new List<object>();
|
||||
foreach (var p in controller.parameters)
|
||||
{
|
||||
parameters.Add(new
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type.ToString(),
|
||||
defaultFloat = p.defaultFloat,
|
||||
defaultInt = p.defaultInt,
|
||||
defaultBool = p.defaultBool
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
path = AssetDatabase.GetAssetPath(controller),
|
||||
name = controller.name,
|
||||
layerCount = controller.layers.Length,
|
||||
parameterCount = controller.parameters.Length,
|
||||
layers,
|
||||
parameters
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object AssignToGameObject(JObject @params)
|
||||
{
|
||||
var controller = LoadController(@params);
|
||||
if (controller == null)
|
||||
return ControllerNotFoundError(@params);
|
||||
|
||||
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
||||
if (go == null)
|
||||
return new { success = false, message = "Target GameObject not found" };
|
||||
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (animator == null)
|
||||
{
|
||||
Undo.RecordObject(go, "Add Animator Component");
|
||||
animator = Undo.AddComponent<Animator>(go);
|
||||
}
|
||||
|
||||
Undo.RecordObject(animator, "Assign AnimatorController");
|
||||
animator.runtimeAnimatorController = controller;
|
||||
EditorUtility.SetDirty(go);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Assigned controller '{controller.name}' to '{go.name}'",
|
||||
data = new
|
||||
{
|
||||
gameObject = go.name,
|
||||
controllerName = controller.name,
|
||||
controllerPath = AssetDatabase.GetAssetPath(controller)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimatorController LoadController(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return null;
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return null;
|
||||
|
||||
return AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
}
|
||||
|
||||
private static object ControllerNotFoundError(JObject @params)
|
||||
{
|
||||
string path = @params["controllerPath"]?.ToString() ?? "(not specified)";
|
||||
return new { success = false, message = $"AnimatorController not found at '{path}'. Provide a valid 'controllerPath'." };
|
||||
}
|
||||
|
||||
private static void CreateFoldersRecursive(string folderPath)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(folderPath))
|
||||
return;
|
||||
|
||||
string parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
|
||||
if (!string.IsNullOrEmpty(parent) && parent != "Assets" && !AssetDatabase.IsValidFolder(parent))
|
||||
CreateFoldersRecursive(parent);
|
||||
|
||||
string folderName = Path.GetFileName(folderPath);
|
||||
if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(folderName))
|
||||
AssetDatabase.CreateFolder(parent, folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3f82c47d9e14b8fa0c5e1b7d4923f16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
internal static class ControllerLayers
|
||||
{
|
||||
public static object AddLayer(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
string layerName = @params["layerName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(layerName))
|
||||
return new { success = false, message = "'layerName' is required" };
|
||||
|
||||
float weight = @params["weight"]?.ToObject<float>() ?? 1f;
|
||||
string blendingModeStr = @params["blendingMode"]?.ToString()?.ToLowerInvariant() ?? "override";
|
||||
|
||||
AnimatorLayerBlendingMode blendingMode = blendingModeStr == "additive"
|
||||
? AnimatorLayerBlendingMode.Additive
|
||||
: AnimatorLayerBlendingMode.Override;
|
||||
|
||||
Undo.RecordObject(controller, "Add Layer");
|
||||
controller.AddLayer(layerName);
|
||||
|
||||
var layers = controller.layers;
|
||||
var newLayer = layers[layers.Length - 1];
|
||||
newLayer.defaultWeight = weight;
|
||||
newLayer.blendingMode = blendingMode;
|
||||
layers[layers.Length - 1] = newLayer;
|
||||
controller.layers = layers;
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Added layer '{layerName}' to '{controllerPath}'",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
layerName,
|
||||
layerIndex = layers.Length - 1,
|
||||
weight,
|
||||
blendingMode = blendingMode.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object RemoveLayer(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
int? layerIndex = @params["layerIndex"]?.ToObject<int?>();
|
||||
string layerName = @params["layerName"]?.ToString();
|
||||
|
||||
if (!layerIndex.HasValue && string.IsNullOrEmpty(layerName))
|
||||
return new { success = false, message = "Either 'layerIndex' or 'layerName' is required" };
|
||||
|
||||
var layers = controller.layers;
|
||||
if (layerIndex.HasValue)
|
||||
{
|
||||
if (layerIndex.Value < 0 || layerIndex.Value >= layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex.Value} out of range (0-{layers.Length - 1})" };
|
||||
|
||||
if (layerIndex.Value == 0)
|
||||
return new { success = false, message = "Cannot remove base layer (index 0)" };
|
||||
|
||||
layerName = layers[layerIndex.Value].name;
|
||||
}
|
||||
else
|
||||
{
|
||||
layerIndex = -1;
|
||||
for (int i = 0; i < layers.Length; i++)
|
||||
{
|
||||
if (layers[i].name == layerName)
|
||||
{
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (layerIndex.Value < 0)
|
||||
return new { success = false, message = $"Layer '{layerName}' not found" };
|
||||
|
||||
if (layerIndex.Value == 0)
|
||||
return new { success = false, message = $"Cannot remove base layer '{layerName}'" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(controller, "Remove Layer");
|
||||
controller.RemoveLayer(layerIndex.Value);
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Removed layer '{layerName}' from '{controllerPath}'",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
layerName,
|
||||
layerIndex = layerIndex.Value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object SetLayerWeight(JObject @params)
|
||||
{
|
||||
string controllerPath = @params["controllerPath"]?.ToString();
|
||||
if (string.IsNullOrEmpty(controllerPath))
|
||||
return new { success = false, message = "'controllerPath' is required" };
|
||||
|
||||
controllerPath = AssetPathUtility.SanitizeAssetPath(controllerPath);
|
||||
if (controllerPath == null)
|
||||
return new { success = false, message = "Invalid asset path" };
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (controller == null)
|
||||
return new { success = false, message = $"AnimatorController not found at '{controllerPath}'" };
|
||||
|
||||
int? layerIndex = @params["layerIndex"]?.ToObject<int?>();
|
||||
string layerName = @params["layerName"]?.ToString();
|
||||
|
||||
if (!layerIndex.HasValue && string.IsNullOrEmpty(layerName))
|
||||
return new { success = false, message = "Either 'layerIndex' or 'layerName' is required" };
|
||||
|
||||
float weight = @params["weight"]?.ToObject<float>() ?? 1f;
|
||||
|
||||
var layers = controller.layers;
|
||||
if (layerIndex.HasValue)
|
||||
{
|
||||
if (layerIndex.Value < 0 || layerIndex.Value >= layers.Length)
|
||||
return new { success = false, message = $"Layer index {layerIndex.Value} out of range (0-{layers.Length - 1})" };
|
||||
|
||||
layerName = layers[layerIndex.Value].name;
|
||||
}
|
||||
else
|
||||
{
|
||||
layerIndex = -1;
|
||||
for (int i = 0; i < layers.Length; i++)
|
||||
{
|
||||
if (layers[i].name == layerName)
|
||||
{
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (layerIndex.Value < 0)
|
||||
return new { success = false, message = $"Layer '{layerName}' not found" };
|
||||
}
|
||||
|
||||
Undo.RecordObject(controller, "Set Layer Weight");
|
||||
var layer = layers[layerIndex.Value];
|
||||
layer.defaultWeight = weight;
|
||||
layers[layerIndex.Value] = layer;
|
||||
controller.layers = layers;
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Set layer '{layerName}' weight to {weight}",
|
||||
data = new
|
||||
{
|
||||
controllerPath,
|
||||
layerName,
|
||||
layerIndex = layerIndex.Value,
|
||||
weight
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7c3e9f1d42b4a6e8f5d0c3b7e9a1f4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Animation
|
||||
{
|
||||
[McpForUnityTool("manage_animation", AutoRegister = false, Group = "animation")]
|
||||
public static class ManageAnimation
|
||||
{
|
||||
private static readonly Dictionary<string, string> ParamAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "clip_path", "clipPath" },
|
||||
{ "controller_path", "controllerPath" },
|
||||
{ "state_name", "stateName" },
|
||||
{ "from_state", "fromState" },
|
||||
{ "to_state", "toState" },
|
||||
{ "parameter_name", "parameterName" },
|
||||
{ "parameter_type", "parameterType" },
|
||||
{ "property_path", "propertyPath" },
|
||||
{ "default_value", "defaultValue" },
|
||||
{ "has_exit_time", "hasExitTime" },
|
||||
{ "exit_time", "exitTime" },
|
||||
{ "layer_index", "layerIndex" },
|
||||
{ "is_default", "isDefault" },
|
||||
{ "relative_path", "relativePath" },
|
||||
{ "function_name", "functionName" },
|
||||
{ "string_parameter", "stringParameter" },
|
||||
{ "float_parameter", "floatParameter" },
|
||||
{ "int_parameter", "intParameter" },
|
||||
{ "event_index", "eventIndex" },
|
||||
{ "layer_name", "layerName" },
|
||||
{ "blending_mode", "blendingMode" },
|
||||
{ "blend_parameter", "blendParameter" },
|
||||
{ "blend_parameter_x", "blendParameterX" },
|
||||
{ "blend_parameter_y", "blendParameterY" },
|
||||
{ "blend_type", "blendType" },
|
||||
};
|
||||
|
||||
private static JObject NormalizeParams(JObject source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
var normalized = new JObject();
|
||||
var properties = ExtractProperties(source);
|
||||
if (properties != null)
|
||||
{
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in source.Properties())
|
||||
{
|
||||
if (string.Equals(prop.Name, "properties", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static JObject ExtractProperties(JObject source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!source.TryGetValue("properties", StringComparison.OrdinalIgnoreCase, out var token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token == null || token.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (token.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JToken.Parse(token.ToString()) as JObject;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new JsonException(
|
||||
$"Failed to parse 'properties' JSON string. Raw value: {token}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key, bool allowAliases)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
if (string.Equals(key, "action", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "action";
|
||||
}
|
||||
if (allowAliases && ParamAliases.TryGetValue(key, out var alias))
|
||||
{
|
||||
return alias;
|
||||
}
|
||||
if (key.IndexOf('_') >= 0)
|
||||
{
|
||||
return StringCaseUtility.ToCamelCase(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private static JToken NormalizeToken(JToken token)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token is JObject obj)
|
||||
{
|
||||
var normalized = new JObject();
|
||||
foreach (var prop in obj.Properties())
|
||||
{
|
||||
normalized[NormalizeKey(prop.Name, false)] = NormalizeToken(prop.Value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (token is JArray array)
|
||||
{
|
||||
var normalized = new JArray();
|
||||
foreach (var item in array)
|
||||
{
|
||||
normalized.Add(NormalizeToken(item));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
JObject normalizedParams = NormalizeParams(@params);
|
||||
string action = normalizedParams["action"]?.ToString();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return new { success = false, message = "Action is required" };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string actionLower = action.ToLowerInvariant();
|
||||
|
||||
if (actionLower.StartsWith("animator_"))
|
||||
{
|
||||
return HandleAnimatorAction(normalizedParams, actionLower.Substring(9));
|
||||
}
|
||||
|
||||
if (actionLower.StartsWith("controller_"))
|
||||
{
|
||||
return HandleControllerAction(normalizedParams, actionLower.Substring(11));
|
||||
}
|
||||
|
||||
if (actionLower.StartsWith("clip_"))
|
||||
{
|
||||
return HandleClipAction(normalizedParams, actionLower.Substring(5));
|
||||
}
|
||||
|
||||
return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: animator_, controller_, or clip_" };
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
McpLog.Error($"[ManageAnimation] Action '{action}' failed: {e}");
|
||||
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object HandleAnimatorAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "get_info": return AnimatorRead.GetInfo(@params);
|
||||
case "get_parameter": return AnimatorRead.GetParameter(@params);
|
||||
case "play": return AnimatorControl.Play(@params);
|
||||
case "crossfade": return AnimatorControl.Crossfade(@params);
|
||||
case "set_parameter": return AnimatorControl.SetParameter(@params);
|
||||
case "set_speed": return AnimatorControl.SetSpeed(@params);
|
||||
case "set_enabled": return AnimatorControl.SetEnabled(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown animator action: {action}. Valid: get_info, get_parameter, play, crossfade, set_parameter, set_speed, set_enabled" };
|
||||
}
|
||||
}
|
||||
|
||||
private static object HandleControllerAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "create": return ControllerCreate.Create(@params);
|
||||
case "add_state": return ControllerCreate.AddState(@params);
|
||||
case "add_transition": return ControllerCreate.AddTransition(@params);
|
||||
case "add_parameter": return ControllerCreate.AddParameter(@params);
|
||||
case "get_info": return ControllerCreate.GetInfo(@params);
|
||||
case "assign": return ControllerCreate.AssignToGameObject(@params);
|
||||
case "add_layer": return ControllerLayers.AddLayer(@params);
|
||||
case "remove_layer": return ControllerLayers.RemoveLayer(@params);
|
||||
case "set_layer_weight": return ControllerLayers.SetLayerWeight(@params);
|
||||
case "create_blend_tree_1d": return ControllerBlendTrees.CreateBlendTree1D(@params);
|
||||
case "create_blend_tree_2d": return ControllerBlendTrees.CreateBlendTree2D(@params);
|
||||
case "add_blend_tree_child": return ControllerBlendTrees.AddBlendTreeChild(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown controller action: {action}. Valid: create, add_state, add_transition, add_parameter, get_info, assign, add_layer, remove_layer, set_layer_weight, create_blend_tree_1d, create_blend_tree_2d, add_blend_tree_child" };
|
||||
}
|
||||
}
|
||||
|
||||
private static object HandleClipAction(JObject @params, string action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "create": return ClipCreate.Create(@params);
|
||||
case "get_info": return ClipCreate.GetInfo(@params);
|
||||
case "add_curve": return ClipCreate.AddCurve(@params);
|
||||
case "set_curve": return ClipCreate.SetCurve(@params);
|
||||
case "set_vector_curve": return ClipCreate.SetVectorCurve(@params);
|
||||
case "create_preset": return ClipPresets.CreatePreset(@params);
|
||||
case "assign": return ClipCreate.Assign(@params);
|
||||
case "add_event": return ClipCreate.AddEvent(@params);
|
||||
case "remove_event": return ClipCreate.RemoveEvent(@params);
|
||||
default:
|
||||
return new { success = false, message = $"Unknown clip action: {action}. Valid: create, get_info, add_curve, set_curve, set_vector_curve, create_preset, assign, add_event, remove_event" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13c8f20dbd31461796f64c421b0a1239
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user