chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
@@ -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: