Skip to content

fix(animation): resolve the Animator on child objects, not just the exact target - #1337

Open
BurakErdemci wants to merge 2 commits into
CoplayDev:betafrom
BurakErdemci:fix/animator-child-lookup
Open

fix(animation): resolve the Animator on child objects, not just the exact target#1337
BurakErdemci wants to merge 2 commits into
CoplayDev:betafrom
BurakErdemci:fix/animator-child-lookup

Conversation

@BurakErdemci

@BurakErdemci BurakErdemci commented Aug 21, 2026

Copy link
Copy Markdown

Description

manage_animation's animator_* actions resolved the Animator with
GetComponent<Animator>() on the target alone. Imported models keep their Animator on the
model root, which is normally a child of the GameObject a user names, so on the most common
rig setup every read and control call failed with No Animator component on 'X' even though
the Animator was right there.

The fix is not just "search children". A naive fallback trades a loud failure for a silent
wrong-target mutation — the same class of problem, made quieter. So this PR also makes an
ambiguous target an error, and makes retargeting visible in the response.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Test update

Changes Made

New AnimatorResolver (MCPForUnity/Editor/Tools/Animation/AnimatorResolver.cs) —
prefers an Animator on the exact target, otherwise looks at descendants, inactive ones
included because a disabled rig is still readable. The seven read/control lookups
(AnimatorRead.GetInfo, AnimatorRead.GetParameter, and AnimatorControl.{Play, Crossfade, SetParameter, SetSpeed, SetEnabled}) all route through it.

Ambiguity is reported, never guessed. Unity's descendant search is depth-first, so a
wrapper holding several rigs returns the first branch however deep — not the nearest rig,
and not what a caller would predict. I measured this on 6000.4.4f1: with Animators on
Root/A/ADeep and Root/B, the winner is ADeep. When more than one descendant carries an
Animator the call now fails and names the candidates, and mutates nothing.

Every successful response names the object that actually changed. A retargeted call used
to report success on the wrapper while a descendant was what moved. This includes the CLI's
own success line for animator_play, which echoed the requested target and so contradicted
the result it had just printed. It matters most for animator_set_parameter, which in Edit
mode writes the shared AnimatorController asset, so the caller needs to see which Animator
answered. Where the tool returns no message at all, the CLI fallback now names no object
rather than guessing the target.

animator_get_info additionally reports animatorGameObject. The existing gameObject
field keeps naming the resolved target, so nothing that reads it changes meaning.

controller_assign deliberately keeps the exact-target lookup. It ADDS an Animator when
none is found; making it search descendants would silently retarget the component it creates.
A test locks that boundary in.

The CLI usage guide still stated the old precondition, so it is updated too.

Compatibility / Package Source

  • Unity version(s) tested: 6000.4.4f1
  • Package source used: file: (TestProjects/UnityMCPTestsfile:../../../MCPForUnity)
  • Resolved commit hash from Packages/packages-lock.json: n/a (local file: source)

Testing/Screenshots/Recordings

  • Python tests (cd Server && uv run pytest tests/ -v) — 1376 passed, 3 skipped
  • Unity EditMode tests — 1206 total, 1140 passed, 0 failed, 66 skipped
  • Unity PlayMode tests
  • Package import/compile check — 0 compile errors

New tests:

Where What
AnimatorResolverTests.cs (new) 12 direct resolver tests: null target, nothing found, exact-target precedence, single descendant, inactive descendant, ambiguity, both error shapes, and the two attribution helpers
ManageAnimationTests.cs a [TestCase] set covering all seven entry points for ambiguity refusal (asserting no rig is mutated), plus child resolution, inactive-child resolution, control parity, response attribution for set_speed/set_enabled/Edit-mode set_parameter, and the controller_assign boundary
test_manage_animation.py 2 CLI tests: the tool's attribution survives to the CLI, and the message-free fallback names no object

I checked that these tests can actually fail, by mutating the resolver three ways and
re-running the animation suite each time:

Mutation Tests that turned red
descendant search removed (the pre-fix lookup) 15
ambiguity detection removed (silent first match) 9 — the seven AllRefuse cases and both ambiguity tests
attribution helpers neutered 3 — set_speed, set_enabled, Edit-mode set_parameter

Reverting the CLI change turns both new Python tests red.

Two of the 17 animation test cases survived every mutation, both on purpose. The
controller_assign test guards behaviour this PR deliberately does not change, and is there
to catch a future edit that pulls it across the boundary. AnimatorGetInfo_TargetAndDescendants HaveAnimators_UsesTheTarget pins exact-target precedence, which none of the three mutations
breaks. I am not counting either as proof of this change.

Worth noting for whoever reads the assertions: the ambiguity cases assert both candidate names
appear in the message, not merely that the call failed. animator_get_parameter and
animator_set_parameter still fail under the silent-match mutation — on a later parameter
check — so a bare success == false assertion would have passed there.

On the 66 skipped: that number is unchanged from before this branch, and none of them are
animation tests. They are [Explicit] process/port tests (38), missing
render-pipeline packages (18), domain-reload stress tests meant to be run by hand (4), and
tests with side effects such as writing real client configs or stopping the server (6).

Documentation Updates

  • I have added/removed/modified tools or resources

No tool, action, or parameter was added, and
uv run python ../tools/generate_docs_reference.py --check reports the generated reference is
up to date. The one documentation change is narrative: Server/src/cli/CLI_USAGE_GUIDE.md
said the target must carry the Animator.

Related Issues

None found — I searched the open issues and PRs and did not find this reported.

Additional Notes

On the Unity version. I could only test locally on 6000.4.4f1, which is not in
tools/unity-versions.json; none of the four matrix versions are installed on this machine.
tools/check-unity-versions.sh skips all four and still exits 0, so please don't read a local
green there as coverage — CI is the real check for the matrix. The change uses no
version-conditional code and no #if UNITY_* blocks.

Two things I found and deliberately did not change. Both are pre-existing and neither is
made worse per call by this PR, but this PR does make the first reachable from more targets,
so they seem worth your judgement rather than my silence:

  1. ClipCreate.Assign also looks up GetComponent<Animator>(), and I left it on the
    exact-target side because it can add a component. Looking closer, that check is really a
    branch selector: with an Animator it returns non-mutating Mecanim guidance; without one
    it adds a legacy Animation component and converts the clip asset to legacy. So after
    this PR, clip_assign on Wrapper takes the legacy branch while animator_get_info on
    the same Wrapper reports the child's Animator — the two disagree about whether Wrapper
    has an Animator. I did not change it because putting a rig under a wrapper and deliberately
    animating the wrapper with a legacy clip looks like a legitimate setup the resolver would
    break. Happy to follow up either way.

  2. Edit-mode animator_set_parameter writes the default onto the shared AnimatorController
    asset, which affects every Animator using that controller, and the response does not say
    so. This is unchanged from beta — I checked the same lines are there — and a direct
    target has always had the same effect, so this PR only broadens which target names reach
    it. Disclosing the controller in that message would be a small, separate improvement.

Unrelated, noticed while checking the CLI's success lines. clip create,
clip create-preset and controller create print a secondary success line using the caller's
raw path, while the tool sanitizes it and appends .anim / .controller. So
animation clip create Assets/Walk creates Assets/Walk.anim and then prints
Created clip at Assets/Walk. Untouched here — mentioning it in case it is useful.

Summary by CodeRabbit

  • New Features

    • Animation commands can now find an Animator on the selected object or its children, including inactive children.
    • Clear messages identify the resolved Animator and list candidates when multiple matches exist.
    • Animator information now includes the associated GameObject name.
  • Bug Fixes

    • Prevented incorrect success messages when animation actions resolve a child object.
    • Improved fallback messages when no response details are available.
  • Documentation

    • Updated animation command guidance to describe child Animator support.
  • Tests

    • Added coverage for resolution, ambiguity handling, inactive children, and response accuracy.

…xact target

Imported models keep their Animator on the model root, which is normally a child
of the GameObject a caller names. The animator_* actions looked the component up
with GetComponent<Animator>() on the target alone, so reads and controls both
failed with "No Animator component" on the most common rig setup.

Route the seven read/control lookups through a shared AnimatorResolver.Find that
falls back to a descendant search. Inactive descendants are included because a
disabled rig is still readable.

Ambiguity is reported, never guessed. Unity's descendant search is depth-first,
so a wrapper holding several rigs returns the first branch however deep - not the
nearest rig, and not what a caller would predict. When more than one descendant
carries an Animator the call fails and names the candidates instead of silently
mutating one of them.

Every successful response names the object that actually changed, including the
CLI's own success line for animator_play, which echoed the requested target and
so contradicted the result it had just printed. This matters most for
animator_set_parameter, which in Edit mode writes the shared AnimatorController
asset, so the caller has to be able to see which Animator answered.

Actions that ADD an Animator (controller_assign) deliberately keep the
exact-target lookup: making them search descendants would silently retarget the
component they create. A test locks that boundary in.

animator_get_info additionally reports animatorGameObject. The existing
gameObject field keeps naming the resolved target.
…he target

The usage guide still stated the old precondition, so a user reading it would
retarget a command to the model child that the resolver now handles on its own.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a79773f-6ffd-4ab2-95fa-ad2be7fb0ec8

📥 Commits

Reviewing files that changed from the base of the PR and between c21bf49 and bb0294d.

📒 Files selected for processing (10)
  • MCPForUnity/Editor/Tools/Animation/AnimatorControl.cs
  • MCPForUnity/Editor/Tools/Animation/AnimatorRead.cs
  • MCPForUnity/Editor/Tools/Animation/AnimatorResolver.cs
  • MCPForUnity/Editor/Tools/Animation/AnimatorResolver.cs.meta
  • Server/src/cli/CLI_USAGE_GUIDE.md
  • Server/src/cli/commands/animation.py
  • Server/tests/test_manage_animation.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AnimatorResolverTests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AnimatorResolverTests.cs.meta
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageAnimationTests.cs

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Animator tools now resolve Animators on targets or unique descendants, including inactive descendants. Ambiguous or missing targets return structured errors. Responses identify resolved objects. CLI output, documentation, and tests cover the new behavior.

Changes

Animator resolution

Layer / File(s) Summary
Animator resolver contract
MCPForUnity/Editor/Tools/Animation/AnimatorResolver.cs, MCPForUnity/Editor/Tools/Animation/AnimatorResolver.cs.meta
Added target-first resolution, inactive descendant search, ambiguity detection, structured errors, and resolved-object descriptions.
Animator tool integration
MCPForUnity/Editor/Tools/Animation/AnimatorRead.cs, MCPForUnity/Editor/Tools/Animation/AnimatorControl.cs
Updated read and control operations to use AnimatorResolver and include resolved Animator context in responses.
CLI behavior and validation
Server/src/cli/commands/animation.py, Server/src/cli/CLI_USAGE_GUIDE.md, Server/tests/test_manage_animation.py, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/*
Updated CLI output and usage text. Added coverage for descendant resolution, ambiguity, inactive objects, mutation safety, controller assignment, and response naming.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to bb029

The change resolves Animator lookup for child and inactive rig objects while refusing ambiguous targets and reporting the object actually affected. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant AnimatorControl
  participant AnimatorResolver
  participant UnityAnimator
  CLI->>AnimatorControl: Send animation command
  AnimatorControl->>AnimatorResolver: Resolve target Animator
  AnimatorResolver->>UnityAnimator: Check target and descendants
  UnityAnimator-->>AnimatorResolver: Return unique match or candidates
  AnimatorResolver-->>AnimatorControl: Return Animator or structured error
  AnimatorControl-->>CLI: Return operation result and resolved-object message
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 42 functions across 7 files. (3 skipped: 3 unsupported.) Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: resolving Animators on child objects.
Description check ✅ Passed The description follows the template and explains the change, compatibility, tests, documentation, scope, and intentional boundaries.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant