Sponsored By

Art Design Deep Dive: Dynamic 2D lighting in Dwerve

In this article, I explain how we added 2D dynamic lighting to our 2D Unity game called Dwerve.

Percy Legendre, Blogger

September 14, 2020

10 Min Read
Game Developer logo in a gray background | Game Developer

Who: Percy Legendre, developer at Half Human Games

My name is Percy Legendre and 2 years ago, Peter Milko and I founded Half Human Games, the small indie studio working on Dwerve, a Zelda-like action RPG with tower defense combat. As indies, we juggle multiple responsibilities. I do programming, game design, writing, and even art at times.

With a small team of just 2 full-time developers, we had to come up with creative ways to make our game look and feel unique without investing too much time.

What: Dynamic lights and shadows for 2D games

We knew from the very beginning that we wanted to push the boundaries of SNES-style pixel graphics with Dwerve. We wanted the player to dungeon crawl through dimly lit dungeons to further immerse them in our fantasy world.

When it came to lighting, a combination of technical and creative skills helped us add beautiful 2D lighting to Dwerve using a Unity Asset called Smart Lighting 2D. Hopefully you can still take away insight from this article even if you use other game engines or lighting systems.

Create a new Unity project using the 2D template and import Smart Lighting 2D into the project. Make a new scene and add a Unity Tilemap by right clicking the Hierarchy window and selecting 2D Object/Tilemap. Make or find a dungeon tileset. Itch.io has a ton of free top-down tilesets. For this article, I am using Pita's Dungeon Tileset which we used as a base for Dwerve assets early on in development. I will recreate the scene below with lighting:

In the sprite import settings, set the Sprite Mode to Multiple, the Pixels Per Unit to 16, and the Filter Mode to Point (no filter). Then open the Sprite Editor and slice the grid into 16 x 16 cells.

You will notice that many sprites are larger than 16 x 16 pixels and are cut into 2 or 4 separate sprite slices. Fix each one by deleting the extra sprite slices and resizing the other to encompass the sprite entirely. Make sure to keep the length and width a multiple of 16. In the image below, the red outline shows the generated sprites, and the blue outline shows what you need to make it look like.

Next set Pivot to CustomPivot Unit Mode to Pixels, and Custom Pivot to (8, 8). This makes sure the sprite will align properly to the tilemap grid.

Don't forget to adjust the positions and pivots of the torch sprites and any other large sprites. When you are done, click the Apply button to save the changes.

Next we need to generate tiles from the Tile Palette window. Open the window from the top toolbar by selecting Window/2D/Tile Palette. Then create a new tile palette called Dungeon Palette and save the tile palette to a new folder Assets/Palettes.

To automatically generate tiles from sprites, just drag the sprites from Assets onto the Tile Palette window. When the Select Folder dialog pops up, create a new folder Assets/Tiles and generate the tiles into that folder.

Your Tile Palette window should now look like this:

Before we start painting our tilemap, let's duplicate the Tilemap game object in the Hierarchy window. Rename the first tilemap game object to Ground Tilemap and rename the second one to Obstacle Tilemap. For the Obstacle Tilemap, set the Order in Layer property of the Tilemap Renderer component to 10 so it renders over the Ground Tilemap.

Now paint your tilemap. Make sure to paint the ground tiles on the Ground Tilemap and the walls, pillars, and props on the Obstacles Tilemap. Skip the torches for now, we will add these later. I am going to paint the tilemap shown at the beginning of this article, but feel free to paint your own.

In the image above, take a look at the furniture by the wall in the bottom right. Notice how the wall is rendering over the furniture. We can fix this in the Project Settings. Open the Project Settings window from the top toolbar by selecting Edit/Project Settings.... Then select Graphics. Under the Camera Settings section, set Transparency Sort Mode to Custom Axis and Transparent Sort Axis to (0, 1, 0). Now the furniture tiles should renderer over the wall tiles.

Next we want to add a collider to our tilemap. Add a Tilemap Collider 2D component to the Obstacle Tilemap game object, and tick the Used by Composite checkbox. Then add a Composite Collider 2D component. This will also automatically add a Rigidbody 2D component. Change the rigidbody Body Type to Static. Select the Obstacle Tilemap game object. Your collider should look similar to the image below.

You might notice a problem with this collider. We want the collider to only be at the base of the objects, not entirely encompass the sprites. Open the Sprite Editor window from the texture import settings and edit the custom physics shapes for the sprites by selecting Sprite Editor/Custom Physics Shape in the top right. Edit the custom physics shapes to include the base of the objects as shown in the image below.

Select Obstacle Tilemap in the hierarchy. If the colliders do not update automatically, disable and re-enable the Tilemap Collider 2D component and the colliders will update. Your colliders should now look like this.

Next let's add the torch tiles. Select the Grid game object and create a new tilemap by right clicking and selecting 2D Object/Tilemap. Rename the tilemap Torch Tilemap and set the Order in Layer property of the Tilemap Renderer component to 20.

We want the torch tiles to be animated. Thankfully, Unity 2D Extras includes AnimatedTile.cs. Since we just need that script, you can download only that script and put it in Assets/Scripts.

Now create a new animated tile by right clicking in the Project window and selecting Create/2D/Tiles/Animated Tile. Rename the tile Pillar Torch Tile and select it. In the Inspector, click the lock icon to lock the Inspector. This prevents it from inspecting a sprite when we select the sprite to drag it into the box.

Drag the torch sprites into the box. Now set both Minimum Speed and  Maximum Speed to 10. Finally, click the lock icon to unlock the Inspector. Repeat these last couple of steps to create the Wall Torch Tile. Do not forget to unlock the Inspector when you are done.

Now drag the newly created torch tiles onto the Tile Palette window like we did earlier with the sprites. Note that we will use these new animated torch tiles and not the ones that got auto-generated earlier.

Now paint a couple of torches on the Torch Tilemap. Keep in mind the torch on the left that sits higher is for pillars while the one on the right that sits lower is for walls. When you are done, enter Play Mode so you can see the animated torches.

Now we are ready to add lighting. Create the Lighting Manager 2D by right clicking the Hierarchy and selecting 2D Light/Light Manager. If you enter Play Mode, the entire screen will be black since there are no lights.

Add a light source by right clicking the Hierarchy and selecting 2D Light/Light Source. Rename the game object to Torch Light and drag it into a Assets/Prefabs to create a prefab. Now duplicate the Torch Light and drag it over each torch. Place it over the ground just in front of the pillar or wall, so that the light source itself is not located inside any obstacle.

Enter Play Mode to check out the lighting.

You will notice that walls and obstacles do not block the light. Exit Play Mode and add a Lighting Tilemap Collider 2D component to the Obstacle Tilemap game object. Enter Play Mode to see how it looks.

Not exactly what we want. The shadows casts around the tile instead of the physics shapes. By default, the Collision Type is set to Tile. Since we want shadows to cast around the base of the obstacles, set Collision Type to Sprite Custom Physics Shape. Enter Play Mode. It should now like this this:

These shadows also look a bit too dark. Let's add more transparency. Open the Lighting 2D window from the top toolbar by selecting Tools/Lighting 2D. Click the Preferences tab if it is not already selected. Set Shadow Darkness to .85. Let's also give our light a warmer tint and make the light size smaller. Open the Torch Light prefab and set the Color property to a light tan color. I decided to use #FFAF64. Next set the Size property to 3. Enter to Play Mode to check out the changes.

Next let's update our torch light to include a nice glow and particles. Open the Torch Light prefab and make the root game object an empty game object, and move the Lighting Source 2D component to a child game object called Light Source. Create two more game objects under Torch Light called Light Glow and Ember Particles. Your Hierarchy should look like the image below.

The light source is positioned on the ground, but we want the glow and embers over the torch. Set the Position of the Light Glow and Ember Particles game objects to (0, .5, 0). Next add a SpriteRenderer component to the Light Glow game object. Set the Color to a light yellow color. I decided to go with #CD7319. Also set the Order in Layer to 20 so it renders over any obstacles. Then set the Sprite to a soft circle texture. I made a custom texture that looks like a donut. Feel free to download and use it.

Next create a new material called Additive Particle Material in Assets/Materials and set the Shader to Legacy Shaders/Particles/Additive. Set the Material property to use this new material. Let's also add a script to make the glow flicker. Download SpriteAlphaFlicker.cs and put it in Assets/Scripts. Then add the component to the Light Glow game object.  Enter Play Mode to check it out.

Next let make ember particles. Add a Particle System component to the Ember Particles game object. Here are the settings I used:

Now the torch has little embers that float upwards. Enter Play Mode to check out the flickering glow and the ember particles.

Let's also add a vignette to darken the edges of the screen. Open the Package Manager window from the top toolbar by selecting Window/Package Manager. Then select Post Processing from the package list and click Install. Then create a new layer called Post Processing.

Next add the Post Process Layer component to the Main Camera game object. Under the Volume blending section, set Layer to Post Processing.

Create a game object called Post Process Volume and set the game object layer to Post Processing.  Add the Post Process Volume component to it and tick the Is Global checkbox. Then press the New button to create a new profile. Click Add effect.../Unity/Vignette. Here are the settings I decided to use.

I also decided to bump the brightness and contrast a bit. Click Add effect.../Unity/Color Grading. Here are the settings I decided to use.

Now enter Play Mode to see how it looks with the vignette and color grading.

Next let's add a light source that follows the mouse around so we can see the dynamic lighting in action. Download FollowMouse.cs and put it in Assets/Scripts. Duplicate one of your torch game objects and rename it Mouse Torch. Add the Follow Mouse component to it. Finally, disable the Light Glow and Ember Particles child objects. Enter Play Mode and move the mouse around so see the dynamic lighting in action.

Why?

Dungeon crawling through dimly lit dungeons with dynamic lighting sets the mood and makes the environments come to life. It further immerses the player in the various cave biomes that exist in the game world. Our goal was never to stick to the strict "old-school pixel art" with pixel perfect cameras and limited color palettes. Our goal, in addition to invoking nostalgia with SNES-inspired pixel graphics, is to immerse the player in a deep fantasy world. Adding dynamic lighting helped us a accomplish this.

Result

It took us a long time to find a lighting system that had all of the features we wanted for Dwerve: tilemap support, soft shadows, deep customization, and high performance.  When we discovered Smart Lighting 2D, it had just been released and lacked minor features and had mild performance bottlenecks. Neck deep in development with still a long road ahead, we took a risk and decided to stick with the asset. And we are glad we did!

It took a lot of experimentation to get the aesthetic we wanted. The developer FunkyCode provided immense support, added missing features, and optimized performance bottlenecks over time. Smart Lighting 2D has since evolved into a mature asset with a stable codebase, deep customization, and high performance. Even still, FunkyCode continues to develop, improve, and support the asset. The various examples, thorough documentation, and active community also provide additional support.

I really hope this modest overview of how we use Smart Lighting 2D to add dynamic lighting to Dwerve will help you improve your next game, and inspire you to find other creative ways to push your aesthetics further.

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like