Maybe there is an easier way to code this, and I’m sure 99% of devs would just unity and copy paste some asset store effect, but I like to code these things myself becausae I like the intellectual challenge, and it gives you total freedom and zero dependencies so here we go:
I just finished coding a ‘shield impact effect’ for Gratuitous Space shooty game, and thought I’d explain how its done. The idea is that the ships are surrounded by energy fields, with various grid patterns, and when laser bullets hit them, they generate a sort of encapsulating ‘wash’ effect over that portion of the shield. To pull this off, I need two things: It needs to be clear where the impact hit the ship, and it needs to feel like it wraps around the target in 3D.
How do you do that in a 2D engine?
The first challenge is getting a graphic that looks like a ship shield in 3D. This is easy. You just get any pattern you want, such as a hex-link pattern:
Then you use the photoshop sphereize distortion filter to give you a nice alpha channel map of a cool space shield:
If I just blap that on top of a spaceship thats hit by lasers, you get a nice effect, but the problem is its non directional, and its pretty clear that its just one image placed on top of another. What I need to do, is to draw this image, but only bits of it at a time, and to wash over the image, revealing it over time.
The way I found was best to do this was to enlarge that texture a lot so it had a ton of empty space, and then use it at twice the size. The reason for this will become apparent, but I’m not working with this:
What I then do, for my shield effect is to use a sort of 2D mesh to wrap around that image over time. That wrap-around effect will originate from the point where the bullet hits the shield. To do this, I get the bullet position, then get the angle from shield center to bullet center, and travel the exact shield radius along that angle from the shield center. That gets me an EXACT location on the perimeter of the shield, even if the bullet is super fast and has already moved inside the perimeter. This is important!
So I now have the impact location, and I want to kind of ‘wash’ the image of the sphere into the players view over time like this:
The red dot is the impact point, the yellow rectangle gets thicker over time as it washes over the sphere. To do this, I ‘virtually’ place the sphere texture on the screen, but do not render it. Its there just as a placeholder for where the ‘full’ sphere would be if it was all revealed. I then calculate how to position that rectangle, and over time I stretch it so it completely covers the target sphere. In order to render the composite image, I render the yellow rectangle, but when I get the UV values of each vertex in the rectangle, I actually look up where they are in relation to my oversized virtual sphere and use that UV value, but the texture of the sphere.
This is why I had to enlarge the canvas the sphere is on, so that those UV values make sense and are still 0 to 1 in both directions. When I do this, I get a cool effect, but its basically a watermelon (shown here with early rubbish shield texture)
To fix that, I just need to set the color values of the ‘inner’ vertices at the top of the rectangle to be fully transparent, and I then get a nice fade effect over the sphere. To be honest, that looked fine, and I was happy with it, but even though NOBODY would notice, something annoyed me…
When you imagine something wrapping around a sphere like this, from a top-down view, you would notice that the speed at which the ‘frontier’ washes over the sphere is non-linear. In other words, I’m not taking any account of the curvature of the sphere itself. To do things right, the speed at which that rectangle washes overs the sphere should vary along the length of the top of the rectangle. To do THAT, I need the rectangle to actually be a tri-strip, so I can curve the top edge. And not only that, the speed at which the center point of that edge moves needs to be a nice curve defined as a sine wave…
In other words I need to achieve this:
That wireframe thing is my yellow triangle. Over time it will wash over the whole shield right to the back. here is two overlapping impacts:
You can see whats going on quite easily in the wireframe version there. The game is way too fast and gratuitous for anyone to notice, and TBH I am still tweaking it.
One of the really fiddly bits was getting that curve just right. I had to start at the left hand top of my yellow triangle, and go across the top edge, noting my progress along there. I then interpolated between the extent to which the ‘height’ of the rectangle came from a fixed point (I picked the top left at the ‘back’ of the shield’) or from my sine-eave inspired non-linear curve of the point that represented the center of the rectangle.
At the end of all that I then basically have a single tri-strip. I then run some separate code to derive UVs from the ‘virtual’ sprite, and just render it. For those who care about the details, here is the code for what I’ve described:
void GUI_ShieldRipple::CalculateVerts()
{
//place relative to the shield
float radius = PParent->GetGlowSprite()->Width / 2;
//we have a tristrip here that is angled at RippleAngle and whose bottom center is at
//InitialImpactOffset from the current shield center;
float shieldcenx = PShip->GetWorldPosition().X;
float shieldceny = PShip->GetWorldPosition().Y;
//derive bottom center
float radangle = D3DXToRadian(RippleAngle);
float cosangle = COS(radangle);
float sinangle = SIN(radangle);
//note this is the inverse because the angle is from offset to cen and we want the reverse
// WorldPos.X += (sinangle * ourspeed);
// WorldPos.Y -= (cosangle * ourspeed);
float botcenx = shieldcenx - (sinangle * radius);
float botceny = shieldceny + (cosangle * radius);
//now turn 90 degrees to the left to go to the start
float angletostart = RippleAngle - 90;
if (angletostart < 0) angletostart += 360;
radangle = D3DXToRadian(angletostart);
cosangle = COS(radangle);
sinangle = SIN(radangle);
float botleftx = botcenx + (sinangle * radius);
float botlefty = botceny - (cosangle * radius);
//invert for right
float botrightx = botcenx - (sinangle * radius);
float botrighty = botceny + (cosangle * radius);
//how wide is each block of the tri-strip
int prims = (MAXVERTS - 2);
float chunkwidth = PParent->GetGlowSprite()->Width / prims;
//now deduce chunkheight, which is basically our travel over the shield at the center point
//we have non linear progress here, and we deduce that from a cosine wave curve imagined from
//Pi to 2xPi
float cosinput = D3DX_PI + (D3DX_PI * Progress);
float adjusted_progress = cos(cosinput);
//now convert to 0 to 1 instead of -1 to 1
adjusted_progress = (adjusted_progress + 1) / 2;
//this gives us the center value, of the middle of the sphere
float chunkheight = radius * 2 * adjusted_progress;
//get top of the strip above the botleft
radangle = D3DXToRadian(RippleAngle);
cosangle = COS(radangle);
sinangle = SIN(radangle);
float innerx = botleftx + (sinangle * chunkheight);
float innery = botlefty - (cosangle * chunkheight);
//start botleft then up, then to 1 along and down...then up
float currx = botleftx;
float curry = botlefty;
float offsetx = (botrightx - botleftx) / (prims/2);
float offsety = (botrighty - botlefty) / (prims/2);
unsigned long fullcolor = RGBA_MAKE(20, 234, 221, (int)(255.0f * Intensity));
unsigned long nocolor = RGBA_MAKE(20, 234, 221, 0);
for (int n = 0; n < MAXVERTS; n+=2)
{
Verts[n].dvSX = currx;
Verts[n].dvSY = curry;
Verts[n].color = fullcolor;
//to get innerx we need to adjust between the full corner value and the middle adjusted
//value based on our progress
float progress = n / (float)prims;
//convert that progress to a nice curve
progress = sin(progress * D3DX_PI);
//thats the extent to which we get the adjusted value rather than the base value of chunkheight being radius
float newheight = (radius * 2 * (1.0f - progress)) + (chunkheight * progress);
float newinx = currx + (sinangle * newheight);
float newiny = curry - (cosangle * newheight);
Verts[n + 1].dvSX = newinx;
Verts[n + 1].dvSY = newiny;
Verts[n + 1].color = nocolor;
currx += offsetx;
curry += offsety;
}
}
Its a lot of C++ to just generate that effect, but I love coding this stuff. I might change it so that the top left and right points of that rectangle are in fact the mid point, so the curve goes flat then inverts. I think that may look better. Also I’ll fiddle with render states and lightmaps to make it fizz more. In the meantime I uploaded a video to twitter showing it with, and without the wireframe. There is a lot going on, and there can be multiple overlapping effects at once. Thats another reason I needed to use a virtual shield texture for UVs, this way everything lines up perfectly, even with multiple impacts:
Gratuitous Space Shooty Game will be on sale on itch soon