Skip to content

Commit 6da232f

Browse files
[feature] Animation and AnimController (#696)
* Animation First PR * Update ClipPresets to take account of local offset * Update MCPForUnity/Editor/Tools/Animation/ClipCreate.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update for AI fix * Update ClipPresets.cs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 47458b0 commit 6da232f

26 files changed

Lines changed: 5454 additions & 42 deletions

MCPForUnity/Editor/Tools/Animation.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using System;
2+
using Newtonsoft.Json.Linq;
3+
using MCPForUnity.Editor.Helpers;
4+
using UnityEditor;
5+
using UnityEditor.Animations;
6+
using UnityEngine;
7+
8+
namespace MCPForUnity.Editor.Tools.Animation
9+
{
10+
internal static class AnimatorControl
11+
{
12+
public static object Play(JObject @params)
13+
{
14+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
15+
if (go == null)
16+
return new { success = false, message = "Target GameObject not found" };
17+
18+
var animator = go.GetComponent<Animator>();
19+
if (animator == null)
20+
return new { success = false, message = $"No Animator component on '{go.name}'" };
21+
22+
string stateName = @params["stateName"]?.ToString();
23+
if (string.IsNullOrEmpty(stateName))
24+
return new { success = false, message = "'stateName' is required" };
25+
26+
int layer = @params["layer"]?.ToObject<int>() ?? -1;
27+
28+
Undo.RecordObject(animator, "Play Animation State");
29+
animator.Play(stateName, layer);
30+
31+
return new { success = true, message = $"Playing state '{stateName}' on '{go.name}'" };
32+
}
33+
34+
public static object Crossfade(JObject @params)
35+
{
36+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
37+
if (go == null)
38+
return new { success = false, message = "Target GameObject not found" };
39+
40+
var animator = go.GetComponent<Animator>();
41+
if (animator == null)
42+
return new { success = false, message = $"No Animator component on '{go.name}'" };
43+
44+
string stateName = @params["stateName"]?.ToString();
45+
if (string.IsNullOrEmpty(stateName))
46+
return new { success = false, message = "'stateName' is required" };
47+
48+
float duration = @params["duration"]?.ToObject<float>() ?? 0.25f;
49+
int layer = @params["layer"]?.ToObject<int>() ?? -1;
50+
51+
Undo.RecordObject(animator, "Crossfade Animation State");
52+
animator.CrossFade(stateName, duration, layer);
53+
54+
return new { success = true, message = $"Crossfading to '{stateName}' over {duration}s on '{go.name}'" };
55+
}
56+
57+
public static object SetParameter(JObject @params)
58+
{
59+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
60+
if (go == null)
61+
return new { success = false, message = "Target GameObject not found" };
62+
63+
var animator = go.GetComponent<Animator>();
64+
if (animator == null)
65+
return new { success = false, message = $"No Animator component on '{go.name}'" };
66+
67+
string paramName = @params["parameterName"]?.ToString();
68+
if (string.IsNullOrEmpty(paramName))
69+
return new { success = false, message = "'parameterName' is required" };
70+
71+
string paramType = @params["parameterType"]?.ToString()?.ToLowerInvariant();
72+
73+
// Auto-detect type if not specified
74+
if (string.IsNullOrEmpty(paramType))
75+
{
76+
for (int i = 0; i < animator.parameterCount; i++)
77+
{
78+
var p = animator.GetParameter(i);
79+
if (p.name == paramName)
80+
{
81+
paramType = p.type.ToString().ToLowerInvariant();
82+
break;
83+
}
84+
}
85+
86+
if (string.IsNullOrEmpty(paramType))
87+
return new { success = false, message = $"Parameter '{paramName}' not found. Specify 'parameterType' explicitly or check the parameter name." };
88+
}
89+
90+
JToken valueToken = @params["value"];
91+
92+
// In Edit mode, runtime Animator.SetFloat/SetInteger/SetBool are no-ops because
93+
// the Animator graph isn't active. Instead, modify the controller asset's default
94+
// parameter values so changes actually persist.
95+
bool isPlaying = Application.isPlaying;
96+
97+
if (isPlaying)
98+
{
99+
Undo.RecordObject(animator, $"Set Animator Parameter {paramName}");
100+
101+
switch (paramType)
102+
{
103+
case "float":
104+
float fVal = valueToken?.ToObject<float>() ?? 0f;
105+
animator.SetFloat(paramName, fVal);
106+
return new { success = true, message = $"Set float '{paramName}' = {fVal}" };
107+
108+
case "int":
109+
case "integer":
110+
int iVal = valueToken?.ToObject<int>() ?? 0;
111+
animator.SetInteger(paramName, iVal);
112+
return new { success = true, message = $"Set int '{paramName}' = {iVal}" };
113+
114+
case "bool":
115+
case "boolean":
116+
bool bVal = valueToken?.ToObject<bool>() ?? false;
117+
animator.SetBool(paramName, bVal);
118+
return new { success = true, message = $"Set bool '{paramName}' = {bVal}" };
119+
120+
case "trigger":
121+
animator.SetTrigger(paramName);
122+
return new { success = true, message = $"Set trigger '{paramName}'" };
123+
124+
default:
125+
return new { success = false, message = $"Unknown parameter type: {paramType}. Valid: float, int, bool, trigger" };
126+
}
127+
}
128+
else
129+
{
130+
// Edit mode: modify the AnimatorController asset's default parameter values
131+
var controller = animator.runtimeAnimatorController as AnimatorController;
132+
if (controller == null)
133+
return new { success = false, message = $"No AnimatorController assigned to Animator on '{go.name}'. Cannot set parameter defaults in Edit mode." };
134+
135+
var allParams = controller.parameters;
136+
int paramIndex = -1;
137+
for (int i = 0; i < allParams.Length; i++)
138+
{
139+
if (allParams[i].name == paramName)
140+
{
141+
paramIndex = i;
142+
break;
143+
}
144+
}
145+
146+
if (paramIndex < 0)
147+
return new { success = false, message = $"Parameter '{paramName}' not found on controller '{controller.name}'." };
148+
149+
Undo.RecordObject(controller, $"Set Parameter Default {paramName}");
150+
151+
switch (paramType)
152+
{
153+
case "float":
154+
float fVal = valueToken?.ToObject<float>() ?? 0f;
155+
allParams[paramIndex].defaultFloat = fVal;
156+
controller.parameters = allParams;
157+
EditorUtility.SetDirty(controller);
158+
AssetDatabase.SaveAssets();
159+
return new { success = true, message = $"Set float '{paramName}' = {fVal} (default value, Edit mode)" };
160+
161+
case "int":
162+
case "integer":
163+
int iVal = valueToken?.ToObject<int>() ?? 0;
164+
allParams[paramIndex].defaultInt = iVal;
165+
controller.parameters = allParams;
166+
EditorUtility.SetDirty(controller);
167+
AssetDatabase.SaveAssets();
168+
return new { success = true, message = $"Set int '{paramName}' = {iVal} (default value, Edit mode)" };
169+
170+
case "bool":
171+
case "boolean":
172+
bool bVal = valueToken?.ToObject<bool>() ?? false;
173+
allParams[paramIndex].defaultBool = bVal;
174+
controller.parameters = allParams;
175+
EditorUtility.SetDirty(controller);
176+
AssetDatabase.SaveAssets();
177+
return new { success = true, message = $"Set bool '{paramName}' = {bVal} (default value, Edit mode)" };
178+
179+
case "trigger":
180+
return new { success = true, message = $"Trigger '{paramName}' noted (triggers are runtime-only, no default to set)" };
181+
182+
default:
183+
return new { success = false, message = $"Unknown parameter type: {paramType}. Valid: float, int, bool, trigger" };
184+
}
185+
}
186+
}
187+
188+
public static object SetSpeed(JObject @params)
189+
{
190+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
191+
if (go == null)
192+
return new { success = false, message = "Target GameObject not found" };
193+
194+
var animator = go.GetComponent<Animator>();
195+
if (animator == null)
196+
return new { success = false, message = $"No Animator component on '{go.name}'" };
197+
198+
float speed = @params["speed"]?.ToObject<float>() ?? 1f;
199+
200+
Undo.RecordObject(animator, "Set Animator Speed");
201+
animator.speed = speed;
202+
203+
return new { success = true, message = $"Set animator speed to {speed} on '{go.name}'" };
204+
}
205+
206+
public static object SetEnabled(JObject @params)
207+
{
208+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
209+
if (go == null)
210+
return new { success = false, message = "Target GameObject not found" };
211+
212+
var animator = go.GetComponent<Animator>();
213+
if (animator == null)
214+
return new { success = false, message = $"No Animator component on '{go.name}'" };
215+
216+
bool enabled = @params["enabled"]?.ToObject<bool>() ?? true;
217+
218+
Undo.RecordObject(animator, "Set Animator Enabled");
219+
animator.enabled = enabled;
220+
221+
return new { success = true, message = $"Animator {(enabled ? "enabled" : "disabled")} on '{go.name}'" };
222+
}
223+
}
224+
}

MCPForUnity/Editor/Tools/Animation/AnimatorControl.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Newtonsoft.Json.Linq;
4+
using MCPForUnity.Editor.Helpers;
5+
using UnityEngine;
6+
7+
namespace MCPForUnity.Editor.Tools.Animation
8+
{
9+
internal static class AnimatorRead
10+
{
11+
public static object GetInfo(JObject @params)
12+
{
13+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
14+
if (go == null)
15+
return new { success = false, message = "Target GameObject not found" };
16+
17+
var animator = go.GetComponent<Animator>();
18+
if (animator == null)
19+
return new { success = false, message = $"No Animator component on '{go.name}'" };
20+
21+
var parameters = new List<object>();
22+
for (int i = 0; i < animator.parameterCount; i++)
23+
{
24+
var p = animator.GetParameter(i);
25+
parameters.Add(new
26+
{
27+
name = p.name,
28+
type = p.type.ToString(),
29+
defaultFloat = p.defaultFloat,
30+
defaultInt = p.defaultInt,
31+
defaultBool = p.defaultBool
32+
});
33+
}
34+
35+
var layers = new List<object>();
36+
for (int i = 0; i < animator.layerCount; i++)
37+
{
38+
var stateInfo = animator.IsInTransition(i)
39+
? animator.GetNextAnimatorStateInfo(i)
40+
: animator.GetCurrentAnimatorStateInfo(i);
41+
42+
layers.Add(new
43+
{
44+
index = i,
45+
name = animator.GetLayerName(i),
46+
weight = animator.GetLayerWeight(i),
47+
currentStateHash = stateInfo.fullPathHash,
48+
currentStateNormalizedTime = stateInfo.normalizedTime,
49+
currentStateLength = stateInfo.length,
50+
isInTransition = animator.IsInTransition(i)
51+
});
52+
}
53+
54+
var clips = new List<object>();
55+
if (animator.runtimeAnimatorController != null)
56+
{
57+
foreach (var clip in animator.runtimeAnimatorController.animationClips)
58+
{
59+
clips.Add(new
60+
{
61+
name = clip.name,
62+
length = clip.length,
63+
frameRate = clip.frameRate,
64+
isLooping = clip.isLooping,
65+
wrapMode = clip.wrapMode.ToString()
66+
});
67+
}
68+
}
69+
70+
return new
71+
{
72+
success = true,
73+
data = new
74+
{
75+
gameObject = go.name,
76+
enabled = animator.enabled,
77+
speed = animator.speed,
78+
hasController = animator.runtimeAnimatorController != null,
79+
controllerName = animator.runtimeAnimatorController?.name,
80+
applyRootMotion = animator.applyRootMotion,
81+
updateMode = animator.updateMode.ToString(),
82+
cullingMode = animator.cullingMode.ToString(),
83+
parameterCount = animator.parameterCount,
84+
layerCount = animator.layerCount,
85+
parameters,
86+
layers,
87+
clips
88+
}
89+
};
90+
}
91+
92+
public static object GetParameter(JObject @params)
93+
{
94+
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
95+
if (go == null)
96+
return new { success = false, message = "Target GameObject not found" };
97+
98+
var animator = go.GetComponent<Animator>();
99+
if (animator == null)
100+
return new { success = false, message = $"No Animator component on '{go.name}'" };
101+
102+
string paramName = @params["parameterName"]?.ToString();
103+
if (string.IsNullOrEmpty(paramName))
104+
return new { success = false, message = "'parameterName' is required" };
105+
106+
AnimatorControllerParameter found = null;
107+
for (int i = 0; i < animator.parameterCount; i++)
108+
{
109+
var p = animator.GetParameter(i);
110+
if (p.name == paramName)
111+
{
112+
found = p;
113+
break;
114+
}
115+
}
116+
117+
if (found == null)
118+
return new { success = false, message = $"Parameter '{paramName}' not found on Animator" };
119+
120+
object value;
121+
switch (found.type)
122+
{
123+
case AnimatorControllerParameterType.Float:
124+
value = animator.GetFloat(paramName);
125+
break;
126+
case AnimatorControllerParameterType.Int:
127+
value = animator.GetInteger(paramName);
128+
break;
129+
case AnimatorControllerParameterType.Bool:
130+
value = animator.GetBool(paramName);
131+
break;
132+
case AnimatorControllerParameterType.Trigger:
133+
value = animator.GetBool(paramName);
134+
break;
135+
default:
136+
value = null;
137+
break;
138+
}
139+
140+
return new
141+
{
142+
success = true,
143+
data = new
144+
{
145+
name = found.name,
146+
type = found.type.ToString(),
147+
value
148+
}
149+
};
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)