Friday, February 24, 2012

Game Elements 6: Multi-Character Camera


 In my last couple of tutorials I talked about shaders. Today I am going to take a quick diversion from that and go back to talking about the camera. I will be starting with the code I created from game elements tutorial 1, so make sure you check that out so that you can follow along.

I have already explained how to create a camera that follows the players character. However, sometimes we want the camera to follow multiple characters, like if the game is multiplayer. So what I am going to explain today is how to create a camera automatically adjusts so that all of the characters are always in view. If you still do not understand what I mean, check out the video example at the top of the tutorial.

A multiplayer camera can be divided into two separate parts. First we want the camera to move to follow both characters and secondly we want the camera to zoom in and out so that the even if the characters move far apart, they will both always be in view.

To get the camera to move, we first need to know the locations of the objects that we want to follow. Since this is a simple example I have just added a list of positions to the camera class:

public static List<Vector2> ViewTargets = new List<Vector2>();

Now to follow the characters we want to define a rectangle that contains all of the positions and then focus the camera on the center point of this rectangle. Since we want to update the rectangle every time one of the objects moves, we are going to need to recalculate the rectangle each time. So what I have done was create this update function:

public static void Update(GraphicsDevice graphicsdevice)
{
    if (ViewTargets.Count > 0)
    {
        Vector2 min = ViewTargets[0];
        Vector2 max = ViewTargets[0];
        for (int i = 1; i < ViewTargets.Count; i++)
        {
            if (ViewTargets[i].X < min.X) min.X = ViewTargets[i].X;
            else if (ViewTargets[i].X > max.X) max.X = ViewTargets[i].X;
            if (ViewTargets[i].Y < min.Y) min.Y = ViewTargets[i].Y;
            else if (ViewTargets[i].Y > max.Y) max.Y = ViewTargets[i].Y;
        }
        Rectangle rect = new Rectangle((int)min.X, (int)min.Y,
            (int)(max.X - min.X), (int)(max.Y - min.Y));

        Position = new Vector2(rect.Center.X, rect.Center.Y);
    }
}

As you can see, I define the rectangle by finding the min and max values of the positions. Then I get the center point of the rectangle and use that as the focus of the target.

To get this all working, in the main class's update method where we had put in a line to update the camera's position to make it match the position of the object, we now want the position to be calculated in the camera's update method and we also want to pass the camera the positions of the objects. So replace that line with the following, where "position2" is the position of a second object that you can add yourself:

Camera.ViewTargets.Clear();
Camera.ViewTargets.Add(position);
Camera.ViewTargets.Add(position2);
Camera.Update(GraphicsDevice);

Now when you run the game the camera will move so that it will focus on the point halfway between the two objects. However, if you move too far apart the objects will move outside of the view of the camera. So what we want to do is to zoom in and out so that the objects are always on screen.

The steps to figure out how much to zoom are pretty simple. First we need to calculate the difference between the height and width of the screen size and that rectangle that just barely contains all of the objects. Then once we have that we set the zoom value to be equal to the smallest difference. So add this code to the end of the camera's update function:

float widthdiff = ((float)graphicsdevice.Viewport.Width) / ((float)rect.Width);
float heightdiff = ((float)graphicsdevice.Viewport.Height) / ((float)rect.Height);
Zoom = Math.Min(widthdiff, heightdiff);

It you run your program now, you should notice that this works but that one problem remains. Although the characters are always visible on screen, the are always right at the edges of the screen, perhaps even a bit cut off. We would rather have a space between the characters and the edge of the screen. Luckly this is easily to add by simply increase the size of the rectangle. XNA even has a function to do this, just add this line to right after where you first define the rectangle in the camera's update function:

rect.Inflate(200, 200);

You should now have a multi-character camera that automatically moves and zooms to follow the objects you want to be in focus. There are a few obvious extensions to this algorithm, such as putting min and max zoom constraints on the camera or making the camera more gradually move and zoom to the ideal position. However I will leave these as exercises for your to try on your own. Also if you have any questions or create something interesting that you want to share please leave a message in the comments.

Monday, February 20, 2012

Game Elements 5: Vignette and Shader Parameters

In my previous tutorial I gave a quick introduction to shaders. Now you should have a basic understanding of how to write a simple shader, but now I am going to show how you can control the shader through code using parameters.

To demonstrate this we are going to write a new shader that will produce a vignette effect. A Vignette typically refers to a darkened border around an image that draws the viewers attention to the center. Adding a subtle vignette effect to any game is a good way to make it look more eye catching and less amateurish. A more pronounced vignette can also be used when you want to obscure the players view of the edge of the screen, such as when walking through a dark cave.

Subtle Vignette (Left), No Vignette (Center), Strong Vignette (Right)
The algorithm for producing this vignette is not much different than the grayscale effect shown in the last tutorial so I wont bother explaining it.

sampler TextureSampler : register(s0);
float Intensity= 1;

float4 VignettePS(float2 texCoord : TEXCOORD0) : COLOR0
{
    float4 tex = tex2D(TextureSampler, texCoord);
    float2 dist = texCoord - 0.5f;
    dist.x = 1 - dot(dist, dist);
    tex.rgb *= saturate(pow(dist.x, intensity));
    return tex;
}

technique Colored
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 VignettePS();
    }
}

Notice the variable "intensity" is defined outside of the pixel shader function. This intensity value sets how strong the vignette effect appears. Variables defined outside of the function can be modified from your game as follows:

MyEffect.Parameters["Intensity"].SetValue(5.5f);

Now you can use this to pass input parameters to your shaders. For practice, set a couple of buttons to increment and decrement the intensity values. And as always, let me know in the comments if you used this to produce any interesting results.

Friday, February 17, 2012

Game Elements 4: Intro to Shaders

In this tutorial I will be talking about pixel shaders, with examples in C#/XNA4 and HLSL. Shaders are a powerful tool that you can use to make your games really stand out. Many game developers using XNA that I have talked do not know much about shaders and thus do not use them. However, writing simple shaders is actually really easy and can add eye catching effects to your games without a major preformance hit.

A pixel shader is basically a function that you want to run on each pixel of an image. If you try doing this in code you will find it really slow. To improve performance shaders are run on the graphics hardware which is specially designed for quick repetitive operations.

Writing simple shaders is not that difficult but it can get a bit tricky when you try doing more complicated operations, but in this tutorial I will stay pretty simple. Shaders are written in a shader language called HLSL (high level shader language). HLSL looks similar to C# but has some differences, most important are that HLSL has a much more limited selection of functions, mostly just ones for math. Also, because HLSL is trying to be fast it has a lot more in common with C, for example all arrays in fixed size. Since we are only writing simple functions in HLSL these should not be too restricting, but they are good to know and a good function reference can be found in the official documentation.

Example Shaders

The first shader that we are going to write will replace the colors with shades of gray. To start we need to create an effect file. XNA treats shaders like any other type of content, so to create an effect file right click on a folder in your content project in visual studio and click "Add->New Item" then select "Effect File". A new effect file will be generated with place holder functions, most of which is unnecessary for our simple shader. Replace everything in your effect file with the following:

sampler TextureSampler : register(s0);

float4 GrayScalePS(float2 texCoord : TEXCOORD0) : COLOR0 {
    float4 tex = tex2D(TextureSampler, texCoord);

 float intensity = tex.r*0.3 + tex.g*0.59 + tex.b*0.11;
 tex.rgb = float3(intensity, intensity, intensity);
    
    return tex;
}

technique Vignette {
    pass Pass1 {
        PixelShader = compile ps_2_0 GrayScalePS();
    }
}

The important part is in the GrayScalePS function, which takes as texture coordinate as an input. We next use a texture sampler to get the color at that texture coordinate. Then take the weighted sum of the three color components of the texture to get the intensity. The weights determine the luminosity of the image and by adjusting these values you can change how each color component contributes to the brightness. These values though are the most commonly used since they provide a good contrast between different colors. For those that have a better understanding on math you might notice that what we are doing here is actually a dot product, so we can simplify these two lines into one and get the same result:

tex.rgb = dot(tex.rgb, float3(0.3, 0.59, 0.11));

Now to apply this shader to your image go to your game code to the area where you are loading your content. Put in this line to load your effect file, the same way you would load a texture:

Effect MyEffect = Game.Content.Load("Effects/MyEffect");

Next, go to where you are drawing your image. In the sprite batch begin method there is an argument for imputing a shader.

spritebatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise, MyEffect, Matrix.Identity);

Now everything that is drawn between the sprite batch begin and end calls will have this shader applied to it. If you run the game now you will see that everything is gray and colorless.


Congratulations, you have written your first shader. Its not a very complicated one but with slight modifications you could reuse it to tint the screen a certain color, invert colors, or make a sepia tone effect.

Friday, February 3, 2012

Game Elements 3: Drawing to a Texture

Introduction

In this tutorial I am going to explain how in XNA we take what would normally be displayed on the screen and instead draw it to a texture. This will be very useful in future tutorials when we want to add complicated effects. Some things that this can be used for is whole screen shaders, mini maps, split screen, etc. I will also show you how to save this image as a file, so you can make screen shots.

For simplicity I will be using the code from the first game elements tutorial as a starting point so that you can see what parts are specifically related to this example.

Creating a Blank Texture
The first step is creating our blank texture, known in XNA as a render target. Add this local variable to the top of your main class:
private RenderTarget2D cameraViewRenderTarget;

Now before we can start drawing to it we need to initialize it to the correct size. We will initialize it to the size of the screen which we can get from the viewport. Add this somewhere in your load content method:
Viewport viewport = GraphicsDevice.Viewport;
cameraViewRenderTarget = new RenderTarget2D(GraphicsDevice, viewport.Width, viewport.Height);
 
Drawing to our Texture
Since we are now drawing to a render target it probably would make more sense to divide up our draw method. I will create a new method for drawing to the render target. Simply take everything from your draw method and put it in this function. Then, at the start of the method set the graphics device's render target to the one we just created, and at the end of the method set the render target to null.
private void DrawTileMapScene()
{
 GraphicsDevice.SetRenderTarget(cameraViewRenderTarget);
 GraphicsDevice.Clear(Color.CornflowerBlue);

 spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
  SamplerState.LinearClamp, DepthStencilState.Default,
  RasterizerState.CullCounterClockwise,
  null, Camera.GetTransformation(GraphicsDevice));

 for (int x = 0; x < 10; x++)
 {
  for (int y = 0; y < 10; y++)
  {
   spriteBatch.Draw(sandtile,
    new Vector2(x * sandtile.Width, y * sandtile.Height), null, Color.White, 0,
    Vector2.Zero, 1, SpriteEffects.None, 0);
  }
 }

 spriteBatch.Draw(character, position, null, Color.White, 0,
  new Vector2(character.Width / 2, character.Height / 2),
  1, SpriteEffects.None, 0);

 spriteBatch.End();

 GraphicsDevice.SetRenderTarget(null);
}
 
Drawing Texture to the Screen
Now we go back to our original draw method and add this new method to the beginning. After you run this function we need to clear the graphics device since it still contains what we just had drawn. Once this is done the screen will be blank but the render target will have what used to be drawn to the screen. To prove it, we will treat the render target like a texture and draw it to the screen as we would any other texture.

protected override void Draw(GameTime gameTime)
{
 DrawTileMapScene();

 GraphicsDevice.Clear(Color.Transparent);

 Texture2D tex = (Texture2D)cameraViewRenderTarget;
 spriteBatch.Begin();
 spriteBatch.Draw(tex, new Vector2(0,0), null, Color.White, 0,
  Vector2.Zero, 1, SpriteEffects.None, 0);
 spriteBatch.End();

 base.Draw(gameTime);
}

The result is that you can now manipulate everything that would have been drawn to the screen as a single texture rather than manipulating each individual part.

Writing Texture to a File
On a bit of a side note, something we can do with this texture is save it as a file. This will allow us to take screenshots. 

Add this code somewhere in your update method. Now when you press 'p' it will save this texture as a file called "screenshots" in the programs execution folder "..\bin\x86\Debug", or you can specify an absolute path like "c:\screenshot.png" to save it to c.
if (Keyboard.GetState().IsKeyDown(Keys.P))
{
 FileStream filesteam = new FileStream("screenshot.png", FileMode.Create);
 cameraViewRenderTarget.SaveAsPng(filesteam,
  cameraViewRenderTarget.Width, cameraViewRenderTarget.Height);
 filesteam.Close();
}

Download Package
Source code can be found in the download package for this tutorial:
Download Package

Extensions
In my some of my future tutorials I will explain some uses of this, however if you find a particularly interesting use let me know in the comments.