Making simple lighting in a 2D game. Detailed C # and XNA Examples for Beginners
In this publication, I will try to tell you very simply how to quickly and easily make dynamic lighting in a 2D game on XNA; without shaders, normal maps, and in general, without additional resources. The goal will be a beautiful game scene with a night sky, a dark background, illuminated by lanterns, a front background and a game menu. The number of light sources is not limited (within reasonable limits, of course), the form is arbitrary. Only the foreground is illuminated. The reader is encouraged to have basic XNA skills, as there will be a lot of code.

A few words as an introduction. I rummaged through a mountain of articles on how to make dynamic lighting in a 2D game, and it was a revelation to me that despite the abundance of material, most of them are simply useless, because I simply can’t repeat what they write about in the code. There are articles with examples in several lines of code on pseudocode. For a beginner, they are almost useless. There are working examples with shaders that you can even download and run. Mountains of code, 2 light sources and not a word on how to add more. And the theme of shaders for a simple 2D game is a bust. And so the idea of this article was born: to enable the reader to add decent dynamic lighting to their game in 30 minutes, thereby saving a ton of time and nerves.
So let's go. First, some theory and simple examples. Draw a few squares on a gray background:

Here we fill the screen with black. Then draw a gray rectangle box1, and on top of 3 squares: box2, box3 and box4. Now let's try to slightly modify the parameters of the spriteBatch.Begin () method, namely:
We declare the variable blendState as follows:
Or so. What is the same thing:
The result is:

In essence, we declare that by drawing box2 on top of box1, you do not need to overlap the pixels, but mix them by color. Mixing itself is carried out according to the formula:
In other words, if you have 2 pixels with the same coordinates and colors: R: 10 G: 20 B: 255 and R: 1 G: 2 B: 255, then the resulting pixel will come out with the color R: 11 G: 22 B: 255 , i.e. will become brighter. From this we can conclude that playing with the settings of the BlendState class, you can shade and highlight individual areas of sprites, thereby obtaining the desired effect of 2D lighting. To highlight the round area on the sprite a bit, you need to draw a round dark gray sprite on top. To, on the contrary, make the round area darker, again draw a round dark gray sprite, but use the BlendFunction.ReverseSubtract function.
That's all with theory, let's move on to practice. To get a beautiful scene, as in the first picture, we will do the following:

Now all this is displayed on the screen:
Result:

And, in fact, the finished code:
I took the drawings from the game “Craft the World”, although I myself have nothing to do with the game. An attentive reader may note strange artifacts on the edges of the building and trees - this is all because I rather casually cut a screenshot in Photoshop. The lighting technique itself does not practically affect the display speed, so the overall performance of the game should not suffer. The finished project for Visual Studio 2010 can be downloaded here: xnagames.codeplex.com/releases/view/136161
I hope this article will be useful for beginner igrodelov. Good luck!

A few words as an introduction. I rummaged through a mountain of articles on how to make dynamic lighting in a 2D game, and it was a revelation to me that despite the abundance of material, most of them are simply useless, because I simply can’t repeat what they write about in the code. There are articles with examples in several lines of code on pseudocode. For a beginner, they are almost useless. There are working examples with shaders that you can even download and run. Mountains of code, 2 light sources and not a word on how to add more. And the theme of shaders for a simple 2D game is a bust. And so the idea of this article was born: to enable the reader to add decent dynamic lighting to their game in 30 minutes, thereby saving a ton of time and nerves.
So let's go. First, some theory and simple examples. Draw a few squares on a gray background:

GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
spriteBatch.Draw(box1, Vector2.Zero, Color.White);
spriteBatch.Draw(box2, new Vector2(40, 50), Color.White);
spriteBatch.Draw(box4, new Vector2(150, 50), Color.White);
spriteBatch.Draw(box3, new Vector2(260, 50), Color.White);
spriteBatch.End();
Here we fill the screen with black. Then draw a gray rectangle box1, and on top of 3 squares: box2, box3 and box4. Now let's try to slightly modify the parameters of the spriteBatch.Begin () method, namely:
spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);
We declare the variable blendState as follows:
var blendState = BlendState.Additive;
Or so. What is the same thing:
var blendState = new BlendState();
blendState.AlphaBlendFunction = BlendFunction.Add;
blendState.AlphaDestinationBlend = Blend.One;
blendState.AlphaSourceBlend = Blend.SourceAlpha;
blendState.BlendFactor = Color.White;
blendState.ColorBlendFunction = BlendFunction.Add;
blendState.ColorDestinationBlend = Blend.One;
blendState.ColorSourceBlend = Blend.SourceAlpha;
blendState.ColorWriteChannels = ColorWriteChannels.All;
blendState.ColorWriteChannels1 = ColorWriteChannels.All;
blendState.ColorWriteChannels2 = ColorWriteChannels.All;
blendState.ColorWriteChannels3 = ColorWriteChannels.All;
blendState.MultiSampleMask = -1;
The result is:

In essence, we declare that by drawing box2 on top of box1, you do not need to overlap the pixels, but mix them by color. Mixing itself is carried out according to the formula:
- (source * sourceBlendFactor) blendFunction (destination * destinationBlendFactor)
- (box2.RGB * BlendState.ColorSourceBlend) BlendFunction.Add (box1.RGB * BlendState.ColorDestinationBlend)
- (box2.RGB * Blend.SourceAlpha) + (box1.RGB * BlendState.One)
- (box2.RGB) + (box1.RGB) (if the pictures are not transparent)
In other words, if you have 2 pixels with the same coordinates and colors: R: 10 G: 20 B: 255 and R: 1 G: 2 B: 255, then the resulting pixel will come out with the color R: 11 G: 22 B: 255 , i.e. will become brighter. From this we can conclude that playing with the settings of the BlendState class, you can shade and highlight individual areas of sprites, thereby obtaining the desired effect of 2D lighting. To highlight the round area on the sprite a bit, you need to draw a round dark gray sprite on top. To, on the contrary, make the round area darker, again draw a round dark gray sprite, but use the BlendFunction.ReverseSubtract function.
That's all with theory, let's move on to practice. To get a beautiful scene, as in the first picture, we will do the following:
- Cooking sprite with the background. To do this, draw the entire background on one sprite (RenderTarget2D);
- Preparing a sprite with a foreground;
- Preparing a sprite with shadows. To do this, take the sprite from the second step and at the points of the light sources draw black round sprites - these are areas that will not be obscured.

Now all this is displayed on the screen:
- We draw the night sky;
- Draw a sprite with the background;
- Once again, we draw a sprite with the background, but with a coefficient of -0.9 (see code). Get a dark background;
- Draw the foreground;
- Draw a sprite with shadows, coefficient -0.9;
- We draw the menu.
Result:

And, in fact, the finished code:
// 1. Готовим спрайт с задним планом.
GraphicsDevice.SetRenderTarget(backgroundSprite);
GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin();
spriteBatch.Draw(background, Vector2.Zero, Color.White);
spriteBatch.End();
// 2. Готовим спрайт с передним планом.
GraphicsDevice.SetRenderTarget(foregroundSprite);
GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin();
spriteBatch.Draw(foreground, Vector2.Zero, Color.White);
spriteBatch.End();
// 3. Готовим спрайт с тенями.
GraphicsDevice.SetRenderTarget(foregroundShadow);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
spriteBatch.Draw(foregroundSprite, Vector2.Zero, Color.White);
spriteBatch.Draw(light, new Vector2(620, 490), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(100, 500), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(620, 90), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(290, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(400, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(510, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.End();
// Теперь это все выводим на экран.
GraphicsDevice.SetRenderTarget(null);
GraphicsDevice.Clear(Color.Black);
// 1. Рисуем ночное небо.
// 2. Рисуем спрайт с задним планом.
spriteBatch.Begin();
spriteBatch.Draw(sky, Vector2.Zero, Color.White);
spriteBatch.Draw(backgroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();
// Готовим обьект BlendState.
var blendState = new BlendState();
blendState.AlphaBlendFunction = BlendFunction.ReverseSubtract;
blendState.AlphaDestinationBlend = Blend.One;
blendState.AlphaSourceBlend = Blend.BlendFactor;
// Тот самый загадочный коэффициент -0.9 (255 * 0.9 = 230, BlendFunction.ReverseSubtract = -1)
{
blendState.BlendFactor = new Color(230, 230, 230, 255);
blendState.ColorBlendFunction = BlendFunction.ReverseSubtract;
}
blendState.ColorDestinationBlend = Blend.One;
blendState.ColorSourceBlend = Blend.BlendFactor;
blendState.ColorWriteChannels = ColorWriteChannels.All;
blendState.ColorWriteChannels1 = ColorWriteChannels.All;
blendState.ColorWriteChannels2 = ColorWriteChannels.All;
blendState.ColorWriteChannels3 = ColorWriteChannels.All;
blendState.MultiSampleMask = -1;
// 3. Еще раз рисуем спрайт с задним планом, но с коэффициентом -0.9.
spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);
spriteBatch.Draw(backgroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();
// 4. Рисуем передний план.
spriteBatch.Begin();
spriteBatch.Draw(foregroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();
// 5. Рисуем спрайт с тенями, коэффициент -0.9.
spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);
spriteBatch.Draw(foregroundShadow, Vector2.Zero, Color.White);
spriteBatch.End();
// 6. Рисуем меню.
spriteBatch.Begin();
spriteBatch.Draw(menu, Vector2.Zero, Color.White);
spriteBatch.End();
I took the drawings from the game “Craft the World”, although I myself have nothing to do with the game. An attentive reader may note strange artifacts on the edges of the building and trees - this is all because I rather casually cut a screenshot in Photoshop. The lighting technique itself does not practically affect the display speed, so the overall performance of the game should not suffer. The finished project for Visual Studio 2010 can be downloaded here: xnagames.codeplex.com/releases/view/136161
I hope this article will be useful for beginner igrodelov. Good luck!