Now I don't want to give too much away but basically I wanted a very dynamic health bar that would be able to show a wealth of information without shoving a large amount of numbers and pictures at the player. I had sketched out a design a while back and finally got around to implementing it in December.
The first problem I encountered was that I wanted the bar to contain liquid (Blood, basically) as a fun, creative way to display the characters state. I've known about the many various particle methods and spring methods of simulating liquid flow but I wanted something simpler and easy to modify. My first instinct was a shader (XNA - Effect), that skewed the water surface to simulate flow. Here is the result:
The liquid is rendered from a simple rectangle using my custom shader.
Getting this result wasn't easy (though it is fairly simple) as I had very little knowledge of shader programming and even less knowledge of wave functions. To make it worse most other tutorials and articles I found were explaining more complicated methods that I did not want nor need. After a lot of research I found a close enough example and from there managed the rest on my own.
If you would like to know how to render a waterline (I'll be calling it a waterbox from here on) keep reading.
So in my case I needed more then to render just a line of fluctuating water. I needed to render a box of water where the top of it fluctuates and the water level can change. This requires some knowledge of how wave data can be generated.
Sine Waves
So to start with you'll need a basic understanding of how to get wave data. Some quick Google searching would tell you that sine waves are one such popular method. Sine waves are used in many different areas of mathematics, signal processing, and engineering as a smooth mathematical curve that oscillates up and down.
Using the .Net libraries you have access to Math.sin which can generate a wave like set of data. The way it works is that you give a value to the sine function and it returns a number between a certain boundary (-1 ~ 1). As the value you give the sine function increases or decreases, the return value will change but it will always be within the boundaries.
So to get the values you need out of the sin function you will need a few variables beforehand; a consistently increasing value (time), and two multiplier values (wavelength, amplitude).
For the time value I used a combination of two things: The game's elapsed time (in seconds), and a speed variable. The elapsed time measures the amount of time that has passed since the last frame and is often called the delta time. The speed variable is multiplied with the time value to slow it down or speed it up as desired.
The wavelength and amplitude values can simply be defined by you and passed to the shader. The wavelength value determines the distance each wave has from the next. A higher wavelength value more, skinnier waves while a lower wavelength value means longer, thicker waves. The amplitude value has to do with how high and low the waves reach. A bigger amplitude value means higher waves and vice verse.
So here is what it all looks like in code:
fTimer += gameTime.ElapsedGameTime.TotalSeconds * Speed; float val = Math.sin(fTimer) * Amplitude; //fTimer is a float that keeps the time.
The wavelength value isn't in this equation as it basically does the same thing as the speed variable if you're just dealing with numbers. When we get into the shader you'll see it pop up again. Also some example values for Speed and Amplitude are 2 and 5 respectively.
The Texture
Before I get into specific shader code we need to cover one more thing quickly. In order to render the waterbox to the screen we need a texture, and there are a few ways to go about this.
1. Rectangle
One method would be to go into photoshop (or whatever you like) and create the perfect sized rectangle for each and every body of water desired.
Basically this is brute-forcing it. For each body of water in your game you create a new rectangle with the desired dimensions (leaving a little transparent room at the top of the texture for the shader to do it's magic).
2. Pixel
The next method is to go into Photoshop (or again, whatever you want) and create a 1x1 pixel image that will be stretched to the dimensions of each body of water. This method is similar to the next one but requires an image to be created before-hand.
3. Code
The last method (the one I use) doesn't involve creating a texture beforehand at all. Instead you can use a simple method to create the desired texture at runtime. Even if you do not use this method for this purpose it is still a very useful one to have. It is as follows
public static Texture2D CreateTex(Game game, int Width, int Height, Color Color)
{
Texture2D tempTex = new Texture2D(game.GraphicsDevice, Width, Height);
Color[] fillColor = new Color[Width * Height];
for (int i = 0; i < fillColor.Length; i++)
{
fillColor[i] = Color;
}
tempTex.SetData<Color>(fillColor);
return tempTex;
}
You can call this function from within your code to create a rectangular texture with the desired color.
Note: Make sure your texture is a solid white color no matter which option you choose as the code we will be writing will allow us to choose what color the water should be dynamically and change it on the fly.
The Class
You are going to want a class to encapsulate all the functionality of the waterbox so go ahead and create one. I entitled mine 'LiquidBlock'. LiquidBlock is going to need some basic methods to get up and running so give it an Update(GameTime GT, float currentPercent) and a Draw(SpriteBatch SB) method along with an empty constructor.
class LiquidBlock
{
class LiquidBlock
{
public LiquidBlock(){}
public void Update(GameTime GT, float currentPercent){}
public void Draw(SpriteBatch SB){}
}
Now we are going to need some variables so go ahead and add the following above the constructor.
public float Speed, Amplitude, Wavelength; //Sine Wave public Rectangle Bounds; //Placement of the LiquidBlock public Color color; //Color of the water private float fTimer; //Keeps the time private Effect waterEffect; //The effect file private Texture2D texture; //The texture to draw
Now let's finish up the constructor with some parameters. Note that this is using code to create the texture (TextureUtils.CreateTex). Alternatively you can pass in or load the texture you desire.
public LiquidBlock(Game game, Rectangle bounds, float amplitude, float wavelength, float Speed, Color color)
{
waterEffect = game.Content.Load<Effect>("WaterEffect");
texture= TextureUtils.CreateTex(game, 1, 1, Color.White);
Amplitude = amplitude;
Wavelength = wavelength;
this.color = color;
Bounds = bounds;
this.Speed = Speed;
time = 0;
waterEffect.CurrentTechnique = waterEffect.Techniques["Technique1"];
waterEffect.Parameters["amplitude"].SetValue(Amplitude);
waterEffect.Parameters["wavelength"].SetValue(Wavelength);
}
Now most of the work is done. The last two things to do involve the shader but we'll add them in now and explain them in a moment.
First make sure the shader is updated with the latest information it needs.
public void Update(GameTime GT, float currentPercent)
{
waterEffect.Parameters["fTimer"].SetValue(fTimer);
waterEffect.Parameters["down"].SetValue(1.0f - currentPercent);
//currentPercent is what percent of the box should be filled.
}
Then we need to actually run the shader and draw some liquid!
public void Draw(SpriteBatch SB)
{
//SB.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend) must be called before this
waterEffect.CurrentTechnique.Passes["Pass1"].Apply();
SB.Draw(texture, Bounds, Color.White);
}
IMPORTANT - The SpriteBatch must have begun before the Draw method in LiquidBlock gets called otherwise it'll throw an error (or begin it inside the draw function, your choice). Also it must have it's SpriteSortMode set to Immediate to work. Should look like this: SB.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend).
IMPORTANT AGAIN - After the LiquidBlock has been drawn you will want to end the spritebatch otherwise the shader will apply itself to anything drawn after it. So be sure to call SB.End() before drawing anything else.
The Shader
Alright now on to the meat and potatoes of the effect. To start with we want to create a new Effect file. Once you have it open you then need to delete everything in it and replace it with the following:
float fTimer;
float amplitude;
float wavelength;
float down;
sampler TextureSampler;
float4 WaterFunction(float4 Color : COLOR0, float2 TexCoords : TEXCOORD0) : COLOR0
{
}
technique Technique1
{
pass Pass1
{
PixelShader = compile ps_2_0 WaterFunction();
}
}
You can read a good tutorial on basic shader functions and syntax here, or with a quick Google search. Basically the technique runs the shader and the WaterFunction is where everything is going to be happening. The parameters Color and TexCoords are filled in automatically with the color value passed into the spritebatch.Draw() call, and the current pixel's coordinates respectively.
Let me explain the PixelShader briefly, which in this case is the WaterFunction(). When you call SpriteBatch.Draw() what you are doing is asking the computer to render a texture to the screen. How it does that involves a lot of behind the scenes effort. Luckily enough as a programmer we can use shaders to manipulate this behind the scene effort.
When the image is about to get rendered the PixelShader will first run for every pixel in the image. You can do things like invert the color, distort it's position, masking and other things in the shader. In our case we are going to examine each pixel and decide if it is above the wave (and should be transparent) or apart/below the wave (still have color).
The simplest form of wave creation looks like this:
float4 WaterFunction(float4 Color : COLOR0, float2 TexCoords : TEXCOORD0) : COLOR0
{
float 2 Tex = TexCoords;
Tex.y -= down;
Tex.y += sin((Tex.x * wavelength) + (fTimer * wavelength)) * amplitude;
float4 color = {0,0,0,0};
if(Tex.y >= 0)
{
Tex.y += down;
color = tex2D(TextureSampler, Tex);
}
return color * Color;
}
The first thing this function does is store the coordinates of the current pixel (relative to the image) in a float2 which is basically a container that holds two float values. Then it immediately pushes modifies the value with the 'down' variable. What this does is give us our starting waterline.
The next thing that happens is we change the y value again based on what we get from the sin function. Basically how it works is the farther the pixel is to the right, the more the sin function advances along it's wave pattern and the value we get will follow it. To increase the number of waves you increase the wavelength. To increase the height of the waves you increase the amplitude. To increase how fast they move across the screen you go back to the LiquidBlock class and change it's Speed variable.
Now that all the values have been added we need to do some final checks to decide if the pixel should be transparent or not. We do this by checking if the coordinates in the Tex variable are on the screen or not. If they are not on the screen then it means that the pixel we have been evaluating needs to be transparent as it appears above the wave. If it is greater than or equal to 0 then it is beneath the wave and should be drawn as it's color.
float4 finalColor = {0,0,0,0}; //This is a transparent color
if(Tex.y >= 0) //If the value is below 0 it means it is not beneath where we want the wave.
{
Tex.y += down;
color = tex2D(TextureSampler, Tex);
//Since the value was above 0 it was filled with the color of the wave.
}
return color * Color;
Note: If you do not see any wave patterns after running then you probably need to pass in a different value for the 'down' variable. If the 'down' variable is at 0 then you won't see any waves as they exist above the rectangle.
Running this should give you a very monotonous looking wave pattern that scrolls across your rectangular waterbox. Try messing with wavelength and amplitude values to get different sizes of waves.
So by now you may be thinking "Hey, these waves aren't very interesting. How do I spice things up?" Well it's actually very simple. All you need to do is add in more sine function calls with different values. For instance if you add in the following two lines beneath the first you end up with a lumpy looking wave form.
Tex.y += (1 + sin((Tex.x*frequency * 0.5f) + (fTimer*frequency))) * amplitude/2; Tex.y += (1 + sin((Tex.x*frequency*2) + (fTimer*frequency))) * amplitude/2;
At this point feel free to experiment with the sin functions until you find a form you like. I hope you managed to learn something from this tutorial and if you have any questions feel free to ask. Here is my complete LiquidBlock and WaterFunction code.
class LiquidBlock
{
private Effect waterEffect;
private Texture2D blankTex;
public float Amplitude;
public float Wavelength;
public float Speed;
public Color Color;
public Rectangle Bounds;
private float time;
public float Time { get { return time; } }
public LiquidBlock(Game game, Rectangle bounds, float amplitude, float wavelength, float Speed, Color color)
{
waterEffect = game.Content.Load<Effect>("WaterEffect");
blankTex = TextureUtils.CreateTex(game, 1, 1, Color.White);
Amplitude = amplitude;
Wavelength= wavelength;
Color = color;
Bounds = bounds;
this.Speed = Speed;
time = 0;
waterEffect.CurrentTechnique = waterEffect.Techniques["Technique1"];
}
private void UpdateEffect(GameTime GT, float curPercent)
{
time += ((float)GT.ElapsedGameTime.TotalSeconds)*Speed;
waterEffect.Parameters["amplitude"].SetValue(Amplitude);
waterEffect.Parameters["wavelength"].SetValue(Wavelength);
waterEffect.Parameters["fTimer"].SetValue(time);
waterEffect.Parameters["down"].SetValue(1.0f - curPercent);
}
public void Update(GameTime GT, float currentPercent)
{
UpdateEffect(GT, currentPercent);
}
public void Draw(SpriteBatch SB)
{
waterEffect.CurrentTechnique.Passes["Pass1"].Apply();
SB.Draw(blankTex, Bounds, Color * 0.5f);
}
}
float4 WaterFunction(float4 Color : COLOR0, float2 TexCoords : TEXCOORD0) : COLOR0
{
float2 Tex = TexCoords;
Tex.y -= down;
Tex.y += (1 + sin((Tex.x*wavelength) + (fTimer*wavelength))) * amplitude;
Tex.y += (1 + sin((Tex.x*wavelength* 0.5f) + (fTimer*wavelength))) * amplitude/2;
Tex.y += (1 + sin((Tex.x*wavelength*2) + (fTimer*wavelength))) * amplitude/2;
float4 color = {0,0,0,0};
if(Tex.y >= 0)
{
Tex.y += down;
color = tex2D(TextureSampler, Tex);
}
return color * Color;
}

No comments:
Post a Comment