How to get a Street Fighter-like health bar?

You know how in fighting games, after you get hit, a portion of the health goes from white to red for a second, until it finally goes away once the entire combo is done?

You can see here where once the fighter gets hit, the healthbar changes a portion from green to red, then to nothing

Does such a thing exist already in cocos2d?

Yes, cocos2d::ui::LoadingBar does that. Look at the API, but basically call setPercent to change the “progress” or health of a character.

I’ll check that out. I’ve been using ProgressTimer and that works fine as a progress bar, but if LoadingBar does that extra interstitial-like effect, it sounds like it could be just what I needed. Thanks

Tried it out, it works just like ProgressTimer, but you lose some customization. Is there a setting I’m missing to have it do that effect I’m looking for?

Sorry, now that I reread your question it’s the interstitial effect you want… I apologize. In that case, I think you need to create a modified version of progress timer or loading bar and change the update function to have the interstitial effect but I expect you were looking for a more elegant solution (which I don’t have–sorry again!)

Thanks all the same.

Using ProgressTimer is the perfect thing, but you can also use a LoadingBar.
The difference is, that the former is derived from a basic Node, and the latter from an UI node.
You don’t need to modify anything. The health-bar effect is just two progress timers/loading bars layered on top of each other.

The red one is underneath the green one. The red bar you see, is the difference in the percentage between the two progress bars:

Player takes a hit -> green bar is set to 80% immediately -> the red bar appears with a 20% difference -> the red bar is set to 80% over time to get that animated decreasing effect.

The effect is pretty trivial. Two progress timers/loading bars. The first one is acting immediately to the decrease in health, and the second one is using an animated decreasing. Just always decrease to the same amount.
If there is a new hit, while the red bar is still decreasing, the animation is stopped and the bar it is set immediately to the old percentage and the animated decreasing begins from this position.

2 Likes

That’s a good idea. Just add one to the child of the other, and use ProgressTo to slide it down. Thanks, I’ll try that.

Not as a child. If you progress the parent, the child will progress with it. You need them to be separated.

1 Like

You’re right, I’ve got it working now.

I wrote a wrapper class that has two progress timers, front and back. They both have the same properties, only the back one has a different sprite (in my case, a color inverted one). On the wrapper class, I use most of the ProgressTimer’s methods to set scale or set position to apply to both at the same time, and then a helper that scrolls the back progress timer after a set duration.

Hope this helps the next person.

class ProgressBar
{
    public:
        cocos2d::ProgressTimer* front_timer;
        cocos2d::ProgressTimer* back_timer;

        float targeted_percentage = 1.0f;
        ProgressBar(Node* scene, std::string front_sprite_path, std::string back_sprite_path);

        void setAnchorPoint(cocos2d::Vec2 vec2);
        void setPosition(cocos2d::Vec2 vec2);
        void setScale(float scale_x);

        void set_percentage(float percentage); //move both to percentage
        void scroll_to_percentage(float percentage); //scroll front then cut back to percentage

};

ProgressBar::ProgressBar(
        Node* scene, std::string front_sprite_path, std::string back_sprite_path
        )
{
     this->scene = scene;

    Sprite* front_sprite;
    Sprite* back_sprite;
    if (front_sprite_path == "")
    {
        front_sprite = Sprite::create();
    }
    else
    {
        front_sprite = Sprite::create(front_sprite_path);
    };
    if (back_sprite_path == "")
    {
        back_sprite = Sprite::create();
    }
    else
    {
        back_sprite = Sprite::create(back_sprite_path);
    };
    this->front_timer = cocos2d::ProgressTimer::create(front_sprite);
    this->back_timer = cocos2d::ProgressTimer::create(back_sprite);
    
    this->back_timer->setGlobalZOrder(1);
    this->front_timer->setGlobalZOrder(2);
    this->scene->addChild(this->front_timer);
    this->scene->addChild(this->back_timer);

    for (ProgressTimer* timer : {this->front_timer, this->back_timer})
    {
        timer->setType(ProgressTimerType::BAR);
        timer->setBarChangeRate(Vec2(1, 0));
        timer->setAnchorPoint(Vec2(0, 0));
        timer->setPosition(0, 0);
        timer->setVisible(true);
        timer->setPercentage(100);

        timer->setMidpoint(Vec2(0, 0));
    };


};

ProgressBar::~ProgressBar()
{
    this->scene = NULL;
    this->front_timer->removeFromParentAndCleanup(true);
    this->back_timer->removeFromParentAndCleanup(true);
};

void ProgressBar::setAnchorPoint(cocos2d::Vec2 vec2)
{
    this->front_timer->setAnchorPoint(vec2);
    this->back_timer->setAnchorPoint(vec2);
};

void ProgressBar::setPosition(cocos2d::Vec2 vec2)
{
    this->front_timer->setPosition(vec2);
    this->back_timer->setPosition(vec2);
};

void ProgressBar::setScale(float scale)
{
    this->front_timer->setScale(scale);
    this->back_timer->setScale(scale);
};

void ProgressBar::set_percentage(float percentage)
{
    this->front_timer->setPercentage(percentage);
    this->back_timer->setPercentage(percentage);
};
    
void ProgressBar::scroll_to_percentage(float percentage)
{
    if (this->target_percentage == percentage) { return; };

    this->target_percentage = percentage;
    this->back_timer->stopAllActions();

    float duration = 0.5f;
    ProgressTo* front_prog_to = ProgressTo::create(duration, percentage * 100);

    DelayTime* delay = DelayTime::create(duration*2);
    ProgressTo* back_prog_to = ProgressTo::create(0.25f, percentage*100);

    Sequence* front_sequence = Sequence::create(front_prog_to, NULL);
    this->front_timer->runAction(front_sequence);
    Sequence* back_sequence = Sequence::create(delay, back_prog_to, NULL);
    this->back_timer->runAction(back_sequence);
};
1 Like

You never set this->scene. Neither in the constructor nor through any setter.

What does this do?

Before running a new action, you should stop the old one.

1 Like

I can replace that, my scene’s got a scale function that tests the current viewport against a given viewport and scales it appropriately. Its so 1 looks like 1.5 on a viewport that’s 50% bigger. I’ll remove it because it’s not relevant here so much.

That code hasn’t been tested, I ripped it out of my game, and renamed a bunch of stuff so it’s more generic.

I’m trying to find a good way to handle queued actions. It works if the scroll_to_percent calls happen 1 second apart, but you’re right that it fails if you call it too soon.

I think I’d need to constantly push back the front_timer and then only update the back_timer once the front_timer is done its action. I’ll look around for a callback sequence or something.

But you should add the scene through the constructor, as you are even setting it to 0 in the destructor:

this->scene = NULL;

Is someone is using your code, he will scratch his head, why it’s crashing.

1 Like

Okay, it’s been added.

There is no need for pushing anything.
Just add a callback to the front timer’s sequence, which calls the back timer’s action. This guaranties, that the back timer will always be called after the front timer and only if there was an action.

1 Like

Say the percentage changes to 90 from 100%, the front will change to 90 over 1 second, and then the back with follow through after another second has passed. The problem comes if I want the front_timer to change to 80% before the 1 second has elapsed (and the back_timer to have caught up).

Currently, the back is just lagging 1 second behind, but I’m looking to have the back_timer wait until the front_timer has completely stopped moving. So instead of back doing the exact same changes 1 second later, I’d like the back_timer to wait for as long as it can before moving to where the front_timer is.

Just introduce a boolean variable like “isActive”, which is set/unset by the starting/ending back timer. The front timer checks it, stops the back timer and sets it to the new front timer percentage immediately.
This is the same behavior like in the video.

Even easier, when using a sequence action. Every time you start a front timer, stop the sequence action, set the back timer to the front timer percentage and create a new sequence action.

This is exactly what happens, when you add the back progress action to the sequence action of the front timer.

1 Like

I’ve been fooling around with this even more. I think the whole mixup here is because I’ve been calling scroll_to_percentage each frame, even when nothing has been happening. So I’m going to do something like your isActive check to make sure I should update the progress bar. That way I can call the stopAllActions on it, because I know it’s not going to be called unless it needs to.

The sequence is created, when the player is hit. The sequence will update itself.
No need to use isActive either, if you are stopping the sequence and create a new one every time the player is hit.

1 Like

So I got it working as I am looking for, all thanks to your consistent help. I added a target_percentage that gets checked before it does its scrolling, so if the percentage is the same as the last time it was called, it’ll just return without doing anything.

void ProgressBar::scroll_to_percentage(float percentage)
{
    if (this->target_percentage == percentage) { return; };

    this->target_percentage = percentage;
    this->back_timer->stopAllActions();

    float duration = 0.5f;
    ProgressTo* front_prog_to = ProgressTo::create(duration, percentage * 100);

    DelayTime* delay = DelayTime::create(duration*2);
    ProgressTo* back_prog_to = ProgressTo::create(0.25f, percentage*100);

    Sequence* front_sequence = Sequence::create(front_prog_to, NULL);
    this->front_timer->runAction(front_sequence);
    Sequence* back_sequence = Sequence::create(delay, back_prog_to, NULL);
    this->back_timer->runAction(back_sequence);
};

Part of my problem was that I was calling this->back_timer->stopAllActions(), but the sequence with the TargetedAction pointed to the back_timer was running on this->front_timer, so the stopAllActions call had no effect.

1 Like