Skip to content

fix(ui): guard manual Animator.Update against inactive objects#9434

Open
eordano wants to merge 1 commit into
devfrom
fix/animator-update-inactive-guard
Open

fix(ui): guard manual Animator.Update against inactive objects#9434
eordano wants to merge 1 commit into
devfrom
fix/animator-update-inactive-guard

Conversation

@eordano

@eordano eordano commented Jul 19, 2026

Copy link
Copy Markdown
Member

Testing Instructions

  1. This is a very internal change. Start the client and let it reach the world. Watch the Player.log during startup: before this PR, UI bootstrap logged Animator.Update / Rebind errors from panels whose tabs and animated buttons get initialized while still hidden. After this PR the console should have no Animator errors at startup.
  2. Tab switching still animates: Open each panel: Backpack, Settings, Communities, Events, Places, Map/Navmap, Camera Reel, and click through every tab. The newly selected tab should play its ACTIVE animation; deselected tabs should snap back to their resting frame (not freeze mid-animation).
  3. Reopen on a non-default tab ("emotes" in the backpack, sub-sections of settings, etc). In a couple of those panels, select a non-default tab, close the panel, and reopen it. This is the main regression risk: the resting-frame snap is now skipped while the panel is hidden, so we rely on OnEnable re-running the snap when it becomes visible. Verify tabs show the correct selected/unselected visuals on reopen, with no tab stuck in a hover or active pose.
  4. Animated buttons. Hover and click buttons using ButtonWithAnimationView (e.g. panel header buttons, sidebar). Verify hover/click animations still play, and still play after the panel has been closed and reopened.
  5. Rapid toggling. Open/close a panel quickly several times while switching tabs. No console errors, no desynced tab visuals.
wallshot3

The shared UI views snap their animator to its resting frame on enable/toggle
with Rebind() + Update(0). Unity only permits Animator.Update on an
active-in-hierarchy object, but these views are routinely enabled or toggled
while their panel is still hidden during UI bootstrap — producing a burst of
"Can't call Animator.Update on inactive object" errors on every startup.

Guard each manual Update(0) with a gameObject.activeInHierarchy check in the
three shared views that account for the spam:

- TabSelectorView.OnEnable
- ButtonWithAnimationView.OnEnable
- SectionSelectorController.SetAnimationState

Behaviour-preserving: the resting-frame snap only mattered while the object
was visible, and the animator re-evaluates normally once the panel is shown.
Verified in editor Play entering Genesis Plaza: startup count 13 -> 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eordano
eordano requested review from a team as code owners July 19, 2026 18:23
@decentraland-bot decentraland-bot added the ext-contribution Identifies a contribution which was not initiated by a Unity Developer label Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@github-actions

Copy link
Copy Markdown
Contributor

Slack notification sent to #explorer-ext-contributions for external review.
To re-send, delete this comment and re-add the ext-contribution label.

@github-actions
github-actions Bot requested a review from anicalbano July 19, 2026 18:24

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(ui): guard manual Animator.Update against inactive objects

STEP 2 — Root-cause check: PASS

This PR addresses a Unity Animator API constraint: Animator.Rebind() and Animator.Update() require the animator's GameObject to be activeInHierarchy. The activeInHierarchy guard is the standard Unity defensive pattern for this.

Nuance for the OnEnable() call sites (ButtonWithAnimationView, TabSelectorView): Unity only fires OnEnable() when the component is effectively active (activeInHierarchy == true). If ButtonAnimator / tabAnimator is on the same GameObject as the MonoBehaviour, the guard is always true (dead code). The guard is only meaningful if the Animator is serialized to a different (e.g. child) object that may be independently deactivated. The SectionSelectorController.SetAnimationState() guard is unambiguously correct since it's called programmatically.

Deferred initialization is safe: when the parent panel later becomes active, Unity fires OnEnable() again — the guard passes, and Rebind() + Update(0) runs at the right time.

STEP 3 — Design & integration: PASS

No new types, systems, or persistent state introduced. No owner search required.

Teardown trace: No new subscriptions, event hookups, or connections. The existing AddListener/RemoveListener pairs in ButtonWithAnimationView.OnEnable()/OnDisable() and TabSelectorView.OnEnable()/OnDisable() remain balanced. ✅

STEP 4 — Member audit

No new public properties or accessors added or changed.

STEP 5 — Line-level findings

See inline comments. Two P2 findings — no P0 or P1 issues.

Security review

No security issues found. Changes are purely defensive UI animation guards — no user input handling, secrets, auth, networking, or file system operations.

STEP 8 — Non-blocking warnings

None. Main scene not modified.


REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Small defensive guards added to 3 UI MonoBehaviour/controller files; no ECS, async, plugin, or architectural changes.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by unknown (<@unknown>) via Slack

Comment on lines +27 to +33
// Animator.Update is only legal on an active-in-hierarchy object; these buttons
// are often enabled while their panel is still hidden during UI bootstrap.
if (ButtonAnimator.gameObject.activeInHierarchy)
{
ButtonAnimator.Rebind();
ButtonAnimator.Update(0);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Centralization opportunity: The Rebind() + Update(0) pattern with the activeInHierarchy guard is repeated inline across all 3 files in this PR, and there are 10+ additional unguarded Rebind() + Update(0) call sites in the codebase (e.g. BackpackController, PlacesView, EventsView, SettingsController, CommunitiesBrowserView, NavmapController, CameraReelController, GiftingTabManager, AvatarTabNavigator).

The codebase already has AnimatorExtensions.ResetAnimator() doing exactly Rebind() + Update(0f) — but without the guard. Consider adding a safe variant there so all call sites benefit from the fix and the guard logic isn't duplicated:

// In AnimatorExtensions.cs
public static bool TryResetAnimator(this Animator animator)
{
    if (!animator.gameObject.activeInHierarchy) return false;
    animator.Rebind();
    animator.Update(0f);
    return true;
}

Then this block simplifies to:

Suggested change
// Animator.Update is only legal on an active-in-hierarchy object; these buttons
// are often enabled while their panel is still hidden during UI bootstrap.
if (ButtonAnimator.gameObject.activeInHierarchy)
{
ButtonAnimator.Rebind();
ButtonAnimator.Update(0);
}
ButtonAnimator.TryResetAnimator();

if (tabAnimator != null)
// Animator.Update is only legal on an active-in-hierarchy object; tabs are
// frequently enabled while their panel is still hidden during UI bootstrap.
if (tabAnimator != null && tabAnimator.gameObject.activeInHierarchy)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Pre-existing null-check inconsistency (CLAUDE.md §11 "Defensive null-checks against non-null declarations"): tabAnimator is assumed non-null on line 40 (tabAnimator.enabled = true) and line 56 (tabAnimator.enabled = false), but this guard checks tabAnimator != null. Either the field can legitimately be null (in which case lines 40 and 56 also need guards) or it cannot (in which case the null check here is noise).

Since this PR didn't introduce the inconsistency, this is a pre-existing observation — but worth aligning when touching this code. If the field is always assigned via the inspector:

Suggested change
if (tabAnimator != null && tabAnimator.gameObject.activeInHierarchy)
if (tabAnimator.gameObject.activeInHierarchy)

@github-actions

Copy link
Copy Markdown
Contributor

Warnings not reduced: 14668 => 14681 — remove at least one warning to merge.

Warnings/errors in files changed by this PR (31)
File Line Rule Message
Assets/DCL/UI/TabSelectorView.cs 29 CSharpWarnings::CS8618 Non-nullable field 'tabAnimator' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/UI/ButtonWithAnimationView.cs 11 CSharpWarnings::CS8618 Non-nullable property 'Button' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/ButtonWithAnimationView.cs 14 CSharpWarnings::CS8618 Non-nullable property 'ButtonAnimator' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/ButtonWithAnimationView.cs 20 CSharpWarnings::CS8618 Non-nullable property 'ButtonHoverAudio' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/ButtonWithAnimationView.cs 18 CSharpWarnings::CS8618 Non-nullable property 'ButtonPressedAudio' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 36 CSharpWarnings::CS8618 Non-nullable property 'HoverAudio' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 14 CSharpWarnings::CS8618 Non-nullable property 'SelectedBackground' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 20 CSharpWarnings::CS8618 Non-nullable property 'SelectedImage' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 26 CSharpWarnings::CS8618 Non-nullable property 'SelectedText' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 33 CSharpWarnings::CS8618 Non-nullable property 'TabClickAudio' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 11 CSharpWarnings::CS8618 Non-nullable property 'TabSelectorToggle' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 17 CSharpWarnings::CS8618 Non-nullable property 'UnselectedImage' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 23 CSharpWarnings::CS8618 Non-nullable property 'UnselectedText' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/UI/TabSelectorView.cs 29 InconsistentNaming Name 'tabAnimator' does not match rule 'members_should_be_pascal_case'. Suggested name is 'TabAnimator'.
Assets/DCL/UI/ButtonWithAnimationView.cs 11 UnusedAutoPropertyAccessor.Local Auto-property accessor 'Button.set' is never used
Assets/DCL/UI/ButtonWithAnimationView.cs 14 UnusedAutoPropertyAccessor.Local Auto-property accessor 'ButtonAnimator.set' is never used
Assets/DCL/UI/ButtonWithAnimationView.cs 20 UnusedAutoPropertyAccessor.Local Auto-property accessor 'ButtonHoverAudio.set' is never used
Assets/DCL/UI/ButtonWithAnimationView.cs 18 UnusedAutoPropertyAccessor.Local Auto-property accessor 'ButtonPressedAudio.set' is never used
Assets/DCL/UI/TabSelectorView.cs 36 UnusedAutoPropertyAccessor.Local Auto-property accessor 'HoverAudio.set' is never used
Assets/DCL/UI/TabSelectorView.cs 14 UnusedAutoPropertyAccessor.Local Auto-property accessor 'SelectedBackground.set' is never used
Assets/DCL/UI/TabSelectorView.cs 20 UnusedAutoPropertyAccessor.Local Auto-property accessor 'SelectedImage.set' is never used
Assets/DCL/UI/TabSelectorView.cs 26 UnusedAutoPropertyAccessor.Local Auto-property accessor 'SelectedText.set' is never used
Assets/DCL/UI/TabSelectorView.cs 33 UnusedAutoPropertyAccessor.Local Auto-property accessor 'TabClickAudio.set' is never used
Assets/DCL/UI/TabSelectorView.cs 11 UnusedAutoPropertyAccessor.Local Auto-property accessor 'TabSelectorToggle.set' is never used
Assets/DCL/UI/TabSelectorView.cs 17 UnusedAutoPropertyAccessor.Local Auto-property accessor 'UnselectedImage.set' is never used
Assets/DCL/UI/TabSelectorView.cs 23 UnusedAutoPropertyAccessor.Local Auto-property accessor 'UnselectedText.set' is never used
Assets/DCL/UI/ButtonWithAnimationView.cs 36 UnusedMember.Local Method 'OnDisable' is never used
Assets/DCL/UI/TabSelectorView.cs 53 UnusedMember.Local Method 'OnDisable' is never used
Assets/DCL/UI/ButtonWithAnimationView.cs 22 UnusedMember.Local Method 'OnEnable' is never used
Assets/DCL/UI/TabSelectorView.cs 38 UnusedMember.Local Method 'OnEnable' is never used
Assets/DCL/UI/SectionSelectorController.cs 82 UnusedParameter.Local Parameter 'ct' is never used

@eordano eordano added the force-build Used to trigger a build on draft PR label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-contribution Identifies a contribution which was not initiated by a Unity Developer force-build Used to trigger a build on draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants