Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more


Quick And Simple Lighting


Posted by Mark Alexander on 12 July 2018

In this tech blog, we're going to show you how to add a simple surface based lighting layer to your games. By simple, we mean that it won't be able to cast shadows, but it will provide the cover of darkness along with areas of brightness. Also for the sake of simplicity, we'll be basing this tech blog on the YoYo Platformer Demo, which you should install now from the Demos section of the GameMaker Studio 2 Start Page:

Get The YoYo Platformer Demo

Once you have it installed, run it and make sure it works and that you have an idea of how it is all put together. Note that if you prefer to use DnD™ then you will need to go into the Game Options and tick the checkbox labelled Use DnD™as default script type before continuing.

Setting Up

To start with, you need to open up the room editor for the room rGrass, and then add a new instance layer. This layer will be for our lighting controller object, so go ahead and name the layer "Lighting" now:

Add Layer For Lighting

The next thing we need to do here is create a parent object for everything we want to be considered as a light source. For that, create a new object and call it "oLightParent ". Now, click the Parent button and in the Parent Editor, click the + sign to add children to it. We want to add the Player, Ghost and Star objects as children:

Add Children To Parent

We'll also need a sprite to use for drawing the light. You can make your own (it should only use white and be on a transparent background about 256x256 pixels in size), or you can download and use the one shown below:

Light Sprite

Create a new sprite resource and call it "sLight" and then add (or create) your light image. You will need to make sure that the origin of the sprite is set to Middle Center:

Light Resource

With that done, we are now ready to start adding in our code to draw the lighting!

Drawing The Surface

We need to make another object now to act as the controller for all the games lighting. So, make a new object and call it "oLighting", then give it a Create Event. In this event we'll add this simple piece of code:

surf = -1;

Set Variable "surf" to -1

Our lighting is going to use a surface with a subtractive blend mode - essentially we'll be drawing a black rectangle over the screen and then "subtracting" from it using a white sprite - and so we need a variable to hold the unique ID value of the surface we'll be using. We don't create it in this event however, as surfaces are volatile which means that they may be overwritten and removed from memory due to changes in the rendering (things like going fullscreen, or minimising the game will cause the surface to disappear, for example). To catch this we'll be doing all our surface manipulation and drawing in the Draw Event.

Add a Draw Event now and in it add the following:

if !surface_exists(surf)
    var _cw = camera_get_view_width(view_camera[0]);
    var _ch = camera_get_view_height(view_camera[0]);
    surf = surface_create(_cw, _ch);
    draw_rectangle(0, 0, _cw, _cw, false);

DnD Create Surface And Clear It

All we're doing here is checking to see if the surface exists and if it doesn't we create it and clear it. Why do we clear it? Well, a surface is really a memory location that has been "reserved" for drawing, and so if we don't clear the surface on creation, it may contain "garbage" from what was previously in the memory location and when we draw the surface we'll have unwanted artefacts.

We've got our "if" to check if the surface exists, so now we need to add an "else" for when it does exist, like this:

if (surface_exists(surf)) {
var _cw = camera_get_view_width(view_camera[0]);
var _ch = camera_get_view_height(view_camera[0]);
var _cx = camera_get_view_x(view_camera[0]);
var _cy = camera_get_view_y(view_camera[0]);
draw_rectangle(0, 0, _cw, _ch, 0);
with (oLightParent)

draw_surface(surf, _cx, _cy);

Draw Surface Actions

What this does is get the view camera position and size and stores these values in variables. It then draws a rectangle to the surface with an 80% alpha (this is the "darkness" that we are going to illuminate), before changing to a subtractive blend mode. At this point we would draw the lights using the "with" statement (or using the "Applies to" DnD™) - but we'll add that in a moment - before finally drawing the surface to the screen at the camera position.

We could make the surface the size of the room (and if you are not using cameras then that is what you would have to do), but in the demo the camera follows the player and so there is no need to draw anything that isn't going to be seen by the player. This helps keep memory use down and makes it less likely that your game will have issues running on low end hardware.

If you run the project now, you should see the game as before, but now shrouded in darkness... Time to add some lights!

Adding The Lights

As mentioned above, the code/actions shown expect us to do something with the "oLightParent" object in the Draw Event code. The simplest thing to add here would be a draw_sprite function or action with the light sprite, but in the demo, each of the child objects has a different origin offset, and it may be that you'd like to add in extra effects on a per-object basis. To make this possible we'll use a switch function/action to check which child object index is actually running the code and change what is drawn accordingly.

So, in the Draw Event, within the "with" or "Applies to..." you'd have:

var _sw = sprite_width / 2;
var _sh = sprite_height / 2;
case oStar:
    draw_sprite_ext(sLight, 0, x - _cx, y - _cy, 0.5 + random(0.05), 0.5 + random(0.05), 0, c_white, 1);
case oGhost:
    draw_sprite_ext(sLight, 0, x + _sw - _cx, y + _sh - _cy, 0.75, 0.75, 0, c_white, 1);
case oPlayer:
    draw_sprite_ext(sLight, 0, x - _cx, y - _sh - _cy, 1, 1, 0, c_white, 1);            

Draw Light Sprites

Here we're drawing the light sprite at the center of each instance, and we're also using the image x/y scale to change the size of the lit area, with a small random value added to it in the case of the star object to make it twinkle. One thing that's very important is that we're also offsetting the x/y position for drawing by the camera x/y position. This is because when you draw to a surface all drawing will be done from the (0, 0) position, which is the surface origin. By subtracting the camera position from the instance position, we bring it into the correct range to be drawn on the surface.

Run the project again now and you should see how the stars, the ghost and the player all have a "light" around them.


FInal Notes

Okay, this isn't really lighting... technically we're really punching holes in a surface and using those holes to reveal what's underneath, but this technique can be very useful in a great number of situations, and making simple (fake) lighting is just one of them. However, before you go off and start playing with this technique, we need to add one final thing into the project, and that's a Clean Up Event. As we mentioned earlier, surfaces take up memory and if we don't free up that memory when not in use we get a memory leak, as we may be creating a new surface every time we (re)start the room and so the old surface gets de-referenced meaning it can no longer be accessed and just sits there in memory taking up space. Memory leaks will eventually slow down and maybe even crash your game when it's being played, and you want to always be vigilant to avoid them.

So, add a Clean Up event now and in it place this code:

if surface_exists(surf)

Clean Up Event

Now we really are finished and you have leaned how to make fast and simple lighting! Now that you have it all set up, take a moment to test the system using different types of sprites with different shapes, alphas, colours... or maybe try different blend modes and see what happens? Whatever you do, have fun and happy GameMaking!

Back to Top