Rolling Health Meter

Something I struggled to find on the internet was a decent working example of a rolling health meter, a'la Earthbound.

For those not familiar, you have a HP value, and when you take damage, it isn't applied automatically -- instead, your HP instead ticks down to that value, like an odometer in a car. In Earthbound, this is not just a cosmetic effect, and as long as your HP is still ticking down, you're not dead yet, which then leads to other clever mechanics not usually seen in RPGs.

What I had trouble finding was how to a) reduce and b) display the "rolling" effect -- what examples I found were cheap, and simply reduced HP by a fixed amount until the target HP was hit. What I wanted was one where the meter is quicker the more health you're gaining / losing, and slows to a crawl when nearing it's target.

I figured it out, so here it is. It was done in Gamemaker's GML, but should translate relatively well to other languages.


Calculating the numbers was simpler than expected. I used two variables:

hp=120;
delta=0;


Delta is how much HP we're removing. Healing is done by simply setting a negative number as the delta, and works just fine.

For configuration and tweaking's sake, I set some magic numbers as variables, too:

mt_s = 0.05;
mt_f = 40;


I thought of them as "Meter Snap" and "Meter Fraction". Call them what you will, _s is when we should give up and just snap to the final HP value, and _f is how slow the meter should go.

A higher mt_s causes the meter to jump more when it gets close, and a lower mt_f makes the meter drop faster.

I calculated HP in Gamemaker's "step" event, which executes every frame:

if (delta != 0)
{
if (delta > mt_s || delta < -mt_s)
{
hp -= delta/mt_f;
delta -= delta/mt_f;
}
else
{
hp = round(hp-delta);
delta = 0;
}
}


In short, if there is a delta, and so damage to apply (or remove) we subtract a fraction of it from the HP value. In my case, 1/40th. A large delta will cause the meter to drop, but as it gets closer to the destination, the delta value becomes smaller and so it dwindles slower until it's smaller than the snap value.

And if we don't need to apply any damage, it doesn't even bother calculating anything.


Drawing the meter was a little harder; the meter needs to roll -- this generally means using sprites or some sort of texture for each number, and sliding it up and down.

For this, I used a spritesheet listing numbers in descending order, 9-0. For safety's sake, I put an extra 0 at the top, and an extra 9 at the bottom. Each individual number was 8x10, so I wound up with an 8x120 sprite.

I did originally try it the other way around, but the meter was scrolling rolling "up" when counting down, and it was just weird.


I defined some local variables in the rendering method / event -- in Gamemaker, this is done using "var":

var hp_text, hp_ones, hp_tens, hp_hundreds, hp_thousands, tx_dp, ind;

Gamemaker does not require type declarations, and will allow you to assign whatever you want to anything you please until you try and math a string, but I've tried to keep it consistient.

I start by setting everything to 0 -- the meter design means we show all digit positions, so a value is needed.

hp_ones=0;
hp_tens=0;
hp_hundreds=0;
hp_thousands=0;


Next, we parse the HP value as a string, and find a decimal place, if any:

hp_text=string(hp);
tx_dp=string_pos(".",hp_text);


Important note, this works in Gamemaker because string_pos just returns int 0 if it can't find the string in question. A more complex check might be needed in other languages.

We set an index value to 0 just in case:

ind=0;

And start parsing the HP string:

if (tx_dp==0)
{
//No decimal points
ind=string_length(hp_text); //Set index to the end of the string, guaranteed to be the ones column
hp_ones=real(string_copy(hp_text,ind,1));  //Parse the last character of the string as a number
ind -= 1; //Move the index one to the "left".
}
else
{
//Decimal value
ind=tx_dp-1; //Set index one to the left of the decimal place
hp_ones=real(string_copy(hp_text,tx_dp-1,string_length(hp_text)-(tx_dp-1))); //Make the ones column a decimal value
ind -= 1; //Move the index one to the left.
}


Beyond the decimal point business, the number will always be the same:

if (ind > 0) //If there's a tens column, parse it and move to the left 1
{ hp_tens=real(string_copy(hp_text,ind,1)); ind -= 1; }
if (ind > 0) //If there's a hundreds column, parse it and move to the left 1
{ hp_hundreds=real(string_copy(hp_text,ind,1)); ind -= 1; }
if (ind > 0) //If there's a thousands column, parse it and move to the left 1
{ hp_thousands=real(string_copy(hp_text,ind,1)); ind -= 1; }


Again, checking for > 0 is a Gamemaker quirk -- strings are 1-indexed, not 0-indexed. In C#, this would be checking for > -1. Because we've already defined these column values as 0, we can just do nothing if the column isn't present.


Because I don't have an indicator for a negative number, I did some cosmetics to draw a box and change it's color:

if (hp-delta <= 0)
draw_set_color(c_red);
else
draw_set_color(c_dkgray);
draw_roundrect(x+28,y-4,x+66,y+12,false);
draw_set_color(c_white);


And here's the actual meter drawing -- GML-specific in this case:

draw_sprite_part(spr_nums,0,0,100-(hp_thousands*10),8,10,x+32,y);
draw_sprite_part(spr_nums,0,0,100-(hp_hundreds*10),8,10,x+40,y);
draw_sprite_part(spr_nums,0,0,100-(hp_tens*10),8,10,x+48,y);
draw_sprite_part(spr_nums,0,0,100-(hp_ones*10),8,10,x+56,y);


draw_sprite_part() does what it says on the tin: it draws part of a sprite. The important bit is:

100-(hp_ones*10)

I'm drawing an 8x10 segment of the sprite, and just moving that window up the spritesheet depending on the value.

If the ones column was a decimal, this will cause it to show part of the number before / after the current value.

The tens, hundreds and thousands columns won't "roll" like the ones in this case, but you could change that easily enough with some string-combining when setting them.

This system also does not care if you have a negative number, although this is likely just Gamemaker handling it in the background; some sanitation of the tens, hundreds and thousands columns might be needed at some point, as they can and will pick up the negative symbol and parse it as a number.

There's probably room for optimization / improvement, but hopefully if you were looking for an example like I was, it gets you thinking in the right direction.

Comments

Popular posts from this blog

Atom editor: Hide project tab / tree view

Removing the watermark from LIV

Videos not working in Irfanview