Shadows

July 9, 2009 1:26 PM Published by

http://www.xnascratch.com/?p=381

In this tutorial, I’ll show you how to add simple 2D shadows to your 2D game. My original blog post demonstrated that you can create simple 2D Shadows that are realistic, with no additional textures, no new lines of code and no math.
At the end of the blog post, I theorized that you could extend the concept by having dynamic shadows that are repositioned based upon the sun. So I created a simple Sun Cycle to go along with my simple 2D shadows. You can download the source code with lots of comments at the bottom of this tutorial.

2dshadowssimple1

Inspired by other 2D shadow tutorials, I wanted to create a very simple method for adding shadows that I could understand, would add as little code as possible and would add no additional textures. One method I found involved simply using photoshop to make your textures black and transparent and then load those new textures into your game. But then you end up with two textures for every object, the real object and the blackened, transparent object (shadow). It occurred to me that spritebatch.draw will do exactly what this method did in photoshop. Thus you can do 2D shadows in your game without adding the shadow textures.

2dshadowssimple21Ultimately, all I do is redraw my existing texture, make it black and transparent, and reposition it based upon my arbitrary sun position. In order to create a sun cycle, I create variables out of my spritebatch.draw parameters that control position, color and layer depth.

I started with the default XNA 3.0 game template and just started adding code. The first thing I needed were variables to control my shadow. These variables are primarily used in the second spritebatch.draw where I redraw the canopy of my tree as a shadow. My textures are canopy and trunk. I keep them as separate pieces because if you shadow the trunk, it will appear to float over the ground. So I just want to shadow the canopy and leave the trunk flat and on the ground. My textures are designed to be sort of isometric in that they are tilted forward 45 degrees. This works for my little game project but the idea can be applied to any 2D perspective you might choose. I create a rectangle for the canopy so I have easy control over positioning both the canopy and it’s shadow. I create variables for alpha and color because that’s how I’m going to make my canopy texture black and transparent in the second spritebatch.draw. I create a couple of offsets so I can fine tune the shadow position. I have a variable for rotation, again, for positioning, and finally a boolean for sunCycle so I can have a little running demo.

public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private Texture2D canopy;
private Texture2D trunk;
private Rectangle canopyRect;
private byte shadowAlpha;
private Color color;
private int shadowOffsetX;
private int shadowOffsetY;
private float shadowRotation;
private bool sunCycle = false;

public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}

canopy145

trunk145

In my initialize method, I set the screen dimensions and name my little game window. Then I setup the starting values for my variables. I set alpha to 40 but you can play with this value depending on how dark you have your background. The darker your background, the darker you will want your shadow. The real magic of this concept is in the color command. Typically we would just use Color.White and that would make our texture opaque. Instead of saying Color.White, I use one of the overloads in Color. Amazingly, there are 9 overloads for Color. I use #8 which uses bytes to represent R, G, B, A. New Color(byte r, byte g, byte b, byte a). That’s Red, Green, Blue and Alpha. A byte is 0 – 255. Zero is black, 255 is white/opaque. So when you are saying Color.White, it’s the same as if you said New Color(255, 255, 255, 255). The Alpha is transparency which you can set on the fly for your texture. And here is the key… If you make r, g and b all equal Zero, you get black. Set your Alpha to 40. Thus you are saying redraw my texture in a different position, make it black and make it transparent down to 40 instead of fully opaque at 255. I then setup my shadow position variables, the offsets and rotation.

protected override void Initialize()
{
//Set screen dimensions
graphics.PreferredBackBufferWidth = 1280;
graphics.PreferredBackBufferHeight = 720;
graphics.IsFullScreen = false;
graphics.ApplyChanges();
Window.Title = "Simple 2D Shadows";

//Set Shadow Start values.
shadowAlpha = 40;
color = new Color(0, 0, 0, shadowAlpha);
shadowOffsetX = 0;
shadowOffsetY = 20;
shadowRotation = 1f;

base.Initialize();
}

In my load content method, I load my textures and create a rectangle for my canopy texture. I set the width and height of the rectangle to the width and height of the texture. So my rectangle is the same size as my canopy texture.

protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);

//Load canopy and trunk
canopy = Content.Load(@"Canopy145");
trunk = Content.Load(@"Trunk145");
//create rectangle for canopy, sized to width and height of canopy texture
canopyRect = new Rectangle(500, 200, canopy.Width, canopy.Height);

}

My update method is pretty simple. All I’m doing is checking to see if the user hits the space bar. If they do, I set the boolean sunCycle to true. It was originally set to false. I have another If then that checks to see if sunCycle is true. If it is true, then increment my rotation variable, shadowRotation, and then keep incrementing until shadowRotation is equal to 1f and set sunCycle to false. This stops the rotation.

protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();

//Check if user hits spacebar and set sunCycle to true to start sun cycle
if (Keyboard.GetState().IsKeyDown(Keys.Space) && sunCycle == false)
{
sunCycle = true;
}

//if suncycle is true (ie. you hit space bar), increment shadowRotation to rotate shadow texture
if (sunCycle)
{
shadowRotation += .005f;

if (shadowRotation >= 1f) //stop shadowRotation when it is greater than 1.
{
sunCycle = false;
}
}

base.Update(gameTime);
}

The draw method is where all of the real work gets done. First you’ll notice I turned on SpriteSortMode.BackToFront in the spritebatch.begin. This allows me to sort my sprites from back to front on the screen in each draw statemnent. If you don’t do this, your sprites will be drawn based upon the order of your spritebatch.draw commands. Personally, I like to control the ordering or layering of my sprites on the screen. Then I don’t have to worry about the specific order of my draw commands. The first spritebatch.draw simply draws our canopy texture inside of the previously defined canopyRectangle. The source rectangle is null since it is unused. I set Color to white or opaque. No rotation. Origin is unused. No spriteeffects. Layer Depth is set to .2. This keeps the canopy on top of the trunk and shadow, or you could say it’s closer to the camera or your face. 0 is in your face. 1 is the back of your screen.

In the second spritebatch.draw, we redraw the same canopy texture but change a number of parameters in the draw command. Our destination rectangle is the center bottom of the canopy rectangle with an offset variable for X and Y used for fine tuning the position of our shadow. Source rectangle is set to null since it’s unused. The color variable gives us our magic. I set color to black (0, 0, 0, alpha) with alpha(transparency) set to 40. ShadowRotation is our float for rotation. Next is Origin. The rotate variable will rotate around the origin point. We could just use new vector(0,0) but the shadow rotation would then rotate around the top left corner of the rectangle. It works better if you set the origin to the center bottom of the rectangle so that becomes your rotation point. I set the rotation point or the origin at the center of the texture X and at the Bottom Y. We don’t use scale. No spriteeffects. Layer Depth is below or under the canopy. This puts the transparent canopy under the regular canopy.

Finally we draw the trunk. Again, I use origin to find the center of the truck texture and then place the center of the trunk in the center of the canopy rectangle so it is nicely positioned. I offset the center point slightly, just to fine tune it’s position.

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.SkyBlue);

spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None);

spriteBatch.Draw(canopy,
canopyRect,
null,
Color.White,
0f,
new Vector2(0f, 0f),
SpriteEffects.None, 0.2f);

spriteBatch.Draw(canopy,
new Vector2(canopyRect.Center.X + shadowOffsetX,
canopyRect.Bottom shadowOffsetY),
null,
color,
shadowRotation,
new Vector2(canopy.Width / 2, canopy.Height),
1f,
SpriteEffects.None, 0.5f);

spriteBatch.Draw(trunk,
new Vector2(canopyRect.Center.X, canopyRect.Center.Y),
null,
Color.White,
0f,
new Vector2(trunk.Width / 2, trunk.Height / 2 25),
1f,
SpriteEffects.None,
0.5f);

spriteBatch.End();

base.Draw(gameTime);
}
}

So there you have it. A simple 2D shadow that changes depending on where you position your sun. I think it looks pretty good. Now consider that if you kept your sun position the same throughout your game, you could eliminate all of the variables and just put your final values in your second spritebatch.draw command. By doing this, you would be adding 2D shadows with no additional textures, a simple copy of your already existing draw command (with changes to parameters you are passing) and no math.

Of if you’re making some form of RPG, wouldn’t it be cool to have a sun cycle with dynamic shadows all the while being mindful of the file size constraints required by Xbox 360 community game submission.

[Download not found]

Allan Chaney

  • Share/Save/Bookmark

Categorised in:


Tag Cloud