Skip to content

Incorrect cascade opacity behavior #3059

@stephenhurry

Description

@stephenhurry
  • axmol version: 3.0.0-597022e
  • devices test on: iPhone 14 Pro, MacBook Pro (Apple M1 Max), Pixel 6a
  • developing environments
    • NDK version: r27d
    • Xcode version: 26.2
    • cmake version: 4.2.1

Since commit 127ddac, Color32 has replaced Color3B, and a node’s opacity has become part of its color state. While this refactor is understandable, it has introduced multiple incorrect behaviors related to cascade opacity.

Issue 1

Cascade Color: OFF & Cascade Opacity: ON
=> Opacity is not propagated to children when modified via setColor instead of setOpacity.

Reproduction

node->setCascadeColorEnabled(false);
node->setCascadeOpacityEnabled(true);

auto color = node->getColor();
color.a = 127; // change opacity via color
node->setColor(color);

Result

The node’s opacity changes, but its children are unaffected.

Cause

Calling setColor triggers:

  • updateCascadeColor
  • updateDisplayedColor

However:

  • setOpacity is not called
  • updateDisplayedColor only propagates to children when _cascadeColorEnabled == true

As a result, opacity propagation is skipped unless cascade color is enabled

Proposed Fix

Handle opacity cascading inside updateDisplayedColor when _cascadeOpacityEnabled is true:

void Node::updateDisplayedColor(const Color32& parentColor)
{
    ...

    if (_cascadeColorEnabled)
    {
        for (const auto& child : _children)
        {
            child->updateDisplayedColor(_displayedColor);
        }
    }
    else if (_cascadeOpacityEnabled)
    {
        for (const auto& child : _children)
        {
            child->updateDisplayedOpacity(_displayedColor.a);
        }
    }
}

Issue 2

Cascade Opacity: OFF
=> A parent’s opacity change has no effect on the node itself, even when the parent has cascade color and/or cascade opacity enabled.

Reproduction

node->setCascadeOpacityEnabled(false);
node->parent->setOpacity(127); // parent changes opacity

Result

The parent’s opacity changes, but the node (its child) is unaffected.

Cause

In updateDisplayedColor, _cascadeOpacityEnabled is incorrectly used to gate whether the node applies its parent’s alpha:

if (_cascadeOpacityEnabled)
    _displayedColor.a = _realColor.a * parentColor.a / 255.0f;

This causes opacity inheritance from the parent to be disabled entirely when _cascadeOpacityEnabled is false.

Proposed Fix

Always apply parent opacity when computing _displayedColor, and remove the conditional:

void Node::updateDisplayedColor(const Color32& parentColor)
{
    _displayedColor.r = _realColor.r * parentColor.r / 255.0f;
    _displayedColor.g = _realColor.g * parentColor.g / 255.0f;
    _displayedColor.b = _realColor.b * parentColor.b / 255.0f;
    _displayedColor.a = _realColor.a * parentColor.a / 255.0f;
    updateColor();

    ...
}

Suggested Final Implementation

void Node::updateDisplayedColor(const Color32& parentColor)
{
    _displayedColor.r = _realColor.r * parentColor.r / 255.0f;
    _displayedColor.g = _realColor.g * parentColor.g / 255.0f;
    _displayedColor.b = _realColor.b * parentColor.b / 255.0f;
    _displayedColor.a = _realColor.a * parentColor.a / 255.0f;
    updateColor();

    if (_cascadeColorEnabled)
    {
        for (const auto& child : _children)
        {
            child->updateDisplayedColor(_displayedColor);
        }
    }
    else if (_cascadeOpacityEnabled)
    {
        for (const auto& child : _children)
        {
            child->updateDisplayedOpacity(_displayedColor.a);
        }
    }
}

Additional Recommendation

I suggest changing the default value of _cascadeOpacityEnabled to true.

In many common game scenarios, developers frequently adjust the opacity of an entire node hierarchy in one operation, for example:

  • Fading in/out a popup together with its labels, sprites, and UI elements
  • Fading in/out a character’s root node when using skeletal animations (e.g. spawn / death transitions)

Defaulting cascade opacity to true better matches typical usage patterns and reduces the likelihood of subtle visual bugs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions