ID:2466493
 
Okay, so you wanna make a health bar. Time to boot up Dream Maker and make 100 different icons where each one has a tiny bit less full than the last, right? Yeah. No. Let's not do that anymore. It's 2019. We're better than that. You're better than that. DM deserves modern health bars and so can you!

We're gonna learn a bit about masking, which is a technique for specifying which part of an image you want to draw. Anything that's not in the part that you want to draw, won't be.

In essence, this:



Becomes this:



Masking is a powerful technique that can save you quite a lot of time and resources. If you understand the theory behind how it works, there are a huge number of applications for this technique. Once this particular arrow is in your quiver, I'm sure you'll find plenty of targets to fire it at.


Make the pretty:

Shoutout to Zasif, who not only inspired this topic for the day, but also provided me some examples of impossible healthbars to work from. If you see pretty pictures in this post, that's his fault.

Let's get started with some health bar graphics just as an example:



Let's also take a look at what we want the end result of our process to look like:



Now that we have an idea of what we want to achieve, we can start to think about how to break up the images in such a way that we can get results.

We want to separate our images into at the very least, 3 layers. We're going to do 4 layers here, but the 4th layer is very much optional.





The mask icon should cover the entire fill area of what we want to mask. It should be full brightness (#FFFFFF) or rgb(255,255,255). This is used to determine what part of your graphic is visible.

The bg, or background icon should only be the fillable area when the area is completely empty.

The fill icon should only be the fillable area when the area is completely full.

The fg, or foreground icon should be anything that goes outside, or even covers the fillable area. This area is optional, and will not be masked at all.


Structure:

We're going to use 5 objects together to create a single dynamic health bar. These objects will use a very specific set of drawing rules to create the desired effect. Let's go over the structure first.

root {
   mask {
       background {
           fill {
           }
       }
   }

   foreground {
   }
}


We also need to understand how these objects work together.

root KEEP_TOGETHER {
    mask {
        background BLEND_MULTIPLY KEEP_TOGETHER {
           fill {
           }
        }
    }

    foreground {
    }
}


When Dreamseeker goes to draw our structure, it will draw root and all of its children on their own canvas. Mask is then drawn by this process, and then its child, background, which will again create a new canvas and draw itself and fill. When background finishes drawing, it will take that whole canvas and blend it multiplicative against what's already been drawn, which is mask. This will cause the actual masking to happen. Because this is happening already within root's own private canvas, anything outside of mask's white pixel area will actually wind up being completely transparent. Last, foreground gets drawn on root, and root's private canvas will all be drawn onto the screen.

The end result of this, is that all of this:



Becomes this:




Make ugly:

Okay, we understand what we want to achieve, we just need to actually do it.

Let's create objects for all five parts:

//the object prototype for our root object:
obj/maskbar
appearance_flags = KEEP_TOGETHER
mouse_opacity = 0
var
obj/foreground
obj/background
obj/fill
obj/mask

//the object prototypes for our four pieces:

obj/maskpart
layer = FLOAT_LAYER
plane = FLOAT_PLANE

bg
icon_state = "bg"
appearance_flags = KEEP_TOGETHER
blend_mode = BLEND_MULTIPLY
fg
icon_state = "fg"
fill
icon_state = "fill"
mask
icon_state = "mask"

New(loc,icon)
src.icon = icon
..()


Now let's set up maskbar so that it creates and arranges all of its parts into vis_contents:

obj/maskbar
New()
Build()
vis_contents.Add(mask,foreground)
..()

Del()
//break relationships
vis_contents.Remove(mask,foreground)
background.vis_contents -= fill
mask.vis_contents -= background
..()
proc
Build()
//create constituent objects
foreground = new/obj/maskpart/fg(null,icon)
background = new/obj/maskpart/bg(null,icon)
fill = new/obj/maskpart/fill(null,icon)
mask = new/obj/maskpart/mask(null,icon)

//arrange constituent objects
background.vis_contents += fill
mask.vis_contents += background


Last, we need to set up a neat little function we can use to make the fill area move around inside of the masking area:

obj/maskbar
var
width = 0
height = 0
orientation = EAST
proc
setValue(ratio=1.0,duration=0)
//constrain the ratio between 0 and 1
ratio = min(max(ratio,0),1)

//apply orientation factors for fill bar offsets
var/fx = 0, fy = 0
switch(orientation)
if(EAST)
fx = -1
if(WEST)
fx = 1
if(SOUTH)
fy = 1
if(NORTH)
fy = -1

//calculate the offset of the fill bar.
var/invratio = 1-ratio
var/epx = width * invratio * fx
var/epy = height * invratio * fy

//apply the offset to the fill bar
if(duration)
//if a time value has been supplied, animate the transition from the current position
animate(fill,pixel_w=epx,pixel_z=epy,time=duration)
else
//if a time value has not been supplied, instantly set to the new position
fill.pixel_w = epx
fill.pixel_z = epy


Now that we have a generic object for handling masked health bars, we can create a few to test it out:

obj/maskbar/test
icon = 'bartest.dmi'
screen_loc = "CENTER:-81,CENTER"
width = 162
height = 5
orientation = EAST

obj/maskbar/test3
icon = 'bartest.dmi'
screen_loc = "CENTER:-81,CENTER:-14"
width = 162
height = 5
orientation = WEST

obj/maskbar/test2
icon = 'bartest2.dmi'
screen_loc = "CENTER:-50,CENTER:14"
width = 40
height = 38
orientation = NORTH

obj/maskbar/test4
icon = 'bartest2.dmi'
screen_loc = "CENTER:0,CENTER:14"
width = 40
height = 38
orientation = SOUTH


Finally, let's just add some demo code here to make the whole thing work:

mob
var
health = 1
obj/maskbar/bartest
obj/maskbar/bartest2
obj/maskbar/bartest3
obj/maskbar/bartest4

Login()
bartest = new/obj/maskbar/test()
bartest2 = new/obj/maskbar/test2()
bartest3 = new/obj/maskbar/test3()
bartest4 = new/obj/maskbar/test4()
client.screen.Add(bartest,bartest2,bartest3,bartest4)
..()

client
Click()
mob.health = !mob.health
mob.bartest.setValue(mob.health,10)
mob.bartest2.setValue(mob.health,10)
mob.bartest3.setValue(mob.health,10)
mob.bartest4.setValue(mob.health,10)


You can grab a copy of the demo here and try it for yourself:

http://files.byondhome.com/Ter13/UIbars_src.zip

Just click anywhere on the map to toggle the health bars.



Additional notes:

It should be noted that any object you mask will act strangely when interacting with the mouse. You will have to find your own workarounds for dealing with this problem, like creating invisible mouse capture objects that act as proxy objects for the masked ones.
neat masking trick. Hopefully people will stop using 100 icon_state bar files now ;p
That's very beautiful code ya got there.
Yut Put wrote:
step 4: byond 3D

Yut Put wrote:
i wonder how many polygons you could feasibly render with a simple 3d transform library that relies on masking

I've managed about 100-200 polygons per frame, but the masking technique isn't really all that useful for texturing. The texels will be calculated wrong.
This is very nice but if i may ask, I seem to get an issue were the health bar does not go down unless the mobs health is at 0.
Show code in developer help. You've gone wrong somewhere.
Done
Thank you for the Snippet, I'm pretty sure it will help a lot people. I just dont understand something... why width and height are in those values in the example? "162 and 5" for example, I opened the dmi file and the value of it is 166x7, tried to open the image but I found no clue.
its the width and height of the bar itself.
In response to Czoaf
Czoaf wrote:
Thank you for the Snippet, I'm pretty sure it will help a lot people. I just dont understand something... why width and height are in those values in the example? "162 and 5" for example, I opened the dmi file and the value of it is 166x7, tried to open the image but I found no clue.

It's the width/height of the fill area itself.

In fact, 162 is the wrong value, which I realized after writing this, because the shape of the fill area I showed is skewed, so I should not be including both edges of the trapezoidal sections, and instead should only be using one, so my percentages will be a little inaccurate in my example because of that. Notice how at 0%, the trapezoid is well beyond the edge of the bar? That's the inaccuracy I'm talking about.

The correct width of the fill area is actually 158x5:

See this image for how I'm getting those values:



That lightened part of the image is the bit that I'm getting the width/height from in this case. Depending on the shape of your fill area, your results are going to vary slightly.
Since this snippet was posted there are now two new masking modes available as of BYOND 513.

Option 1: The alpha mask filter

Create a obj called the mask container. This will contain the actual bar in its visual contents. The mask container (probably) doesn't need an icon. (There may be an issue where it won't behave right without one, but you can give it a blank icon of the right size.) The container should have the KEEP_TOGETHER flag, and you would give it an alpha mask filter with an icon that covers wherever you want the bar to be visible.

obj/mask_container
appearance_flags = KEEP_TOGETHER

New(newloc, obj/bar/bar)
filters = filter(type="alpha", icon='bar_mask.dmi')
vis_contents = bar

The downside of this approach is that filters do have a small cost to them, which could add up.

Option 2: Inset blending

The other new option is the new BLEND_INSET_OVERLAY mode. When applied to an icon that's in a KEEP_TOGETHER group or plane master, it will draw itself over the top of existing pixels but it will also use them as an alpha mask. Any part of the underlying image that's transparent will stay that way.

obj/mask_container
appearance_flags = KEEP_TOGETHER
icon = 'bar_background.dmi' // serves as a background image and a mask

New(newloc, obj/bar/bar)
vis_contents = bar

obj/bar
...
plane = FLOAT_PLANE
layer = FLOAT_LAYER
blend_mode = BLEND_INSET_OVERLAY

The BLEND_INSET_OVERLAY method does what BLEND_MULTIPLY does in the original code but it can do it all a little bit simpler. You don't need a special white mask for instance.