GameDev Blog: Goblin Rules Football #17: Using Shader Graph to Create Dynamic Outlines

In my last post, I mentioned wanting to add some more visual indicators for the ball carrier by creating an outline around the goblin with the ball. I didn’t want to have to add an outline to each sprite frame in every ball carrying animation, and switch between them and so on, so I decided to use the Universal Render Pipeline and Shader Graphs to do it. The shader graph will look like this:

Before I did this, I would have no idea what any of that means (besides the fact that it’s too small to see anything…) and after I’ve done this…I still don’t really know what it is! But it does create an outline around my goblins!

As far as I know, the basic idea of what the shader graph is doing is this: The shader will look at every pixel in the texture (the sprite image) that uses a material that uses this shader graph. The shader looks at every pixel in the texture and checks if it is transparent or not (if the alpha is greater than 0). If it is transparent, it does nothing. If the pixel is not transparent, that means it is part of the image to be displayed. The shader then checks adjacent pixels in every direction: up, down, left, and right (also in the diagonals if you tell it to). If the adjacent pixels have alpha > 0, it does nothing. If the adjacent pixel is transparent/alpha is 0, that means the pixel being investigated is on the edge of the image and the shader will draw the outline color in that transparent pixel.

After you create a material for the shader graph and apply that material to a sprite render, the end result looks like this:

Drawing an outline with a thickness of 2 pixels

What’s great about this is that the outline is calculated and drawn on each frame, so it will dynamically “update” the outline even when the sprite animates and moves around and so on. Great! The calculation happens every frame, but since it is so simple and is being done on a small number of pixels (each goblin is 64×64 pixels or less) it doesn’t appear to affect performance much at all. Running at 1440p and I get 300+ FPS.

I copied this all from Daniel Ilett‘s tutorial here (the video moves fast so I had to pause a lot and run at 0.75x speed). There are a couple of caveats to the video tutorial, mostly discussed in the comment section thoroughly. The comment:

And after you plug the "Saturate" node into the "w" of the Vector 4, also plug it directly into the Fragment node's "Alpha." That should fix the black/white box issue.

Means to do the following:

This did all require installing the Universal Render Pipeline (URP), which ended up breaking a few things. First, I had to update the “Pixel Perfect Camera” script on my cameras to the URP script, which is called “Experimental”

The old Pixel Perfect Camera script still exists and will silently fail when you use it. When you view the camera in your inspector, though, it does tell you a warning about how it won’t work with URP:

I also had a “blur” shader I copied from somewhere a while ago to do a UI blur when the esc menu was opened. I remade it with shader graphs following this tutorial.

I also decided to modify the “selection circle” indicator after I saw this video from Pixel Cup Soccer:

Pixel Cup Soccer has a spinning circle icon below the players that I liked better than my old selection circle. The old circle kind of got lost in the shadow. It stands out better as the animated, broken circle imo. You can see both the new selection circle and outline in the below video!

Wow! Whoa!

That’s basically all I did in the past week. Also cleaned up some more mundane issues I had in the tutorial scene (I think I’m finally done with that…)

Oh wait! One of the main things I worked on was fixing how players can use power ups with a controller. Before it was with d-pad buttons or by using the right analog stick to select a power up, and then pressing the stick down to use the power up. The d-pad thing was always less than ideal because the player can’t press the d-pad and move with the left stick very easily. The right stick solution I had was also not so great as it took a bit of time to select your power up and then press down. The only reason I had the “selection” thing is because I couldn’t figure out a way so if a player moved the right stick to the left, they’d use the #1/left d-pad power up. Basically, use the right stick to recreate the d-pad buttons. Well, I figured it out! So, if a player wants to use their #2 power up, which for the gamepad is marked as d-pad up, they just have to move the right stick up and it will be used! That took an embarrassingly long time to figure out. I had to create a new control action map that was a 2d vector that was “digital normalized” and set the Up/Down/Left/Right to the Right Stick/Up, etc.

The main issue is that the analog stick is constantly sending it’s position, so calling UsePowerUp after the player moves the stick in a direction will cause UsePowerUp to be called like 100 times in a second. So, I set a bool value to true on the first UsePowerUp call. If the value is true, UsePowerUp wouldn’t be called. The bool would only be set back to false when the right stick action was “cancelled” aka moved back to the neutral position.

Next Steps…

The next thing I want to do is see if I can figure out how to support control rebinding for my control scheme. It will likely be hard or weird, but hopefully it works! DapperDino has a couple of tutorials on it!

Smell ya later, nerds!