How to draw a flashlight in a dark room?

Hey everyone. I have a game concept I'd like to try to make but I'm not sure exactly where to start. I'm just looking for general pointers and can figure it out myself from there (not asking for anyone to write my code for me), I'm just a bit lost on what the approach should actually be.

Basically, I want to draw an image of a room to the screen. And then cover that image with a black box so it's hidden in the dark. And then allow a player to move a small "flashlight" circle through the dark room, represented by a circle of light. I'd like to have the base image show through the "hole" the flashlight creates in the black box. But I'm not sure where to start with "subtracting" from an image I want to draw like that black box. Am I going about it wrong? Is there a better approach to doing this?

For bonus points, I'd like to be able to resize the circle on the fly. And it would be cool to get a dithering effect around the edges of the circle so that it isn't such a hard split from dark to light (maybe this can be done using a preset sprite image for the circle that has the dithered edges already baked into the sprite)? But I can't even work out where to start with drawing a subtraction from that black box so that stuff is getting a little ahead of myself. :sweat_smile:

Many thanks to anyone who can generally point me in the right direction. If it helps, I've been using Lua on a Windows machine for my development so far.

1 Like

There could be loads of more optimal ways to do this, but you could have a flashlight-sized sprite that has the circular area as transparent pixels, and the area outside the circle in black pixels
[black png with transparent circle in the centre:]
image
[how it looks in my paint app:]

like that. You could draw the other parts of the screen as sprites with custom draw functions that just draw black rects above, below and to either side of that flashlight sprite.

Hmm, interesting idea. I was worried that doing something like that where I am drawing rects around a sprite each frame would cause performance issues. But if you think this is the best way to achieve such an effect with the ask. I can give it a shot and see how much slowdown occurrs on my device. Thanks!

If anyone else has any other ideas Is still love to hear them :grin:

see the Image Masks section of the SDK docs

1 Like

If it hits performance, there's a bunch of better ways to do it. For instance, instead of drawing the whole screen and then drawing black over it, you could only draw the bits that are revealed in the flashlight. So instead of clipping all your sprites to the 0,0,400,240 rect you'd clip to the flashlight rect.

Or like Matt says, you could draw your stuff into a separate bitmap and then use a mask to draw it to the screen only where the flashlight reveals it.

Ooh, that last suggestion sounds like what I am after. One thing the first suggestion doesn't allow for either is multiple flashlights/sources of light. For example, if there was a static candle that has its own circle around it, I'd want the two circles to cleanly merge when the flashlight moves over it, and not continue to draw the static edge of the flashlight circle's sprite cutting into the candle's source of light. So in order to have the edges be dynamic like that, I think I'd need to do something more than just draw circles with edges around it, right?

I'll try reading up on masks and see if I have more questions (I probably will) but I definitely appreciate being pointed in the right direction. Thank you!

For multiple light sources... Matt's suggestion would be best I think. You'd draw your scene into a screen-sized image, then update the mask for that image as fully white with black filled circles (or whatever pattern you like) around your light sources, then clear the screen to black and draw your image over that

Thank you all for pointing me in the right direction. I believe I have something working that meets all my above criteria (dithered edges that blend into the darkness, support for multiple sources of light that bleed into each other when overlapping, ability to resize on the fly for light flickering effects, etc):

flashlight

It does require updating the entire screen each frame - it seems like there should be a way to do this that only updates the portion of the screen that are lit up. But I am not sure how to do that at the moment. I think it updates the whole thing because I'm simply creating a new 400, 240 image each frame to use as an image mask. Any ideas on how to make this more efficient, or is this as good as it gets?

It's not the prettiest, but I'll post the source code here for this example in case others have a similar problem in the future (and if I have time later maybe I will add comments or clean it up some as well):

import "CoreLibs/graphics"
import "CoreLibs/sprites"

import "circle"

local pd <const> = playdate
local gfx <const> = pd.graphics

local lightObjects = {}

local function initialize()
	playdate.display.setRefreshRate(30)

	local bgImage = gfx.image.new("images/bg")
	local bgSprite = gfx.sprite.new(bgImage)
	bgSprite:add()
	bgSprite:moveTo(200, 120)

	local staticImage = gfx.image.new(12, 12)
	gfx.pushContext(staticImage)
		gfx.fillCircleAtPoint(6, 6, 6)
	gfx.popContext()
	local staticSprite = gfx.sprite.new(staticImage)
	--staticSprite:add()
	staticSprite:moveTo(88, 8)
	table.insert(lightObjects, staticSprite)

	local circleSprite = Circle(200, 120, 1)
    circleSprite:add()
    circleSprite:moveTo(20, 120)
	table.insert(lightObjects, circleSprite)

	local darknessImage = gfx.image.new(400, 240, gfx.kColorBlack)
	darknessImage:addMask()
	darknessSprite = gfx.sprite.new(darknessImage)
	darknessSprite:add()
	darknessSprite:moveTo(200, 120)
end

local function handleLighting()
	local darknessImage = gfx.image.new(400, 240, gfx.kColorBlack)
	local maskImage = gfx.image.new(400, 240, gfx.kColorWhite)
	gfx.pushContext(maskImage)
		local width, height = maskImage:getSize()

		for i,lightObj in ipairs(lightObjects) do
			local xx, yy = lightObj:getPosition()
			local circle_size, max_rings, core_size = 80 - math.random(-2, 2), 8, 48
			local edge_size = circle_size - core_size

			
			gfx.setColor(gfx.kColorBlack)
			gfx.setDitherPattern(0, gfx.image.kDitherTypeBayer8x8)
			gfx.fillCircleAtPoint(xx, yy, core_size)

			for j = 1,max_rings do
				local ring_strength = (j/max_rings)
				gfx.setColor(gfx.kColorBlack)
				gfx.setDitherPattern(ring_strength, gfx.image.kDitherTypeBayer8x8)
				gfx.fillCircleAtPoint(xx, yy, (edge_size * ring_strength) + core_size)
			end
		end
	gfx.popContext()
	darknessImage:setMaskImage(maskImage)
	darknessSprite:setImage(darknessImage)
end


initialize()

function pd.update()
	handleLighting()
	gfx.sprite.update()
end
2 Likes

It looks delightful!

I was thinking you could do this with light sources as separate sprites to prevent the full screen refresh but you'll run into problems when they overlap. Must be a way, just can't quite think of it.

2 Likes

Looks cool!

The screen is refreshed in rows, and these are quite big, so a partial refresh optimisation probably wouldn't gain much as most rows will be changing.

So you could make a sprite whose image contains all lights. You might have to dynamically change the size of the image in the sprite, just big enough to contain all lights, to get the most out of the approach.

But I'm still unsure if the effort required would be worth it.

1 Like

I made something similar for my game

sanitycheck

need to optimize the code, since is updating every frame, im losing some fps.
my approach was using masks and not a completely black background (I need some transparency so you can see the enemies) .
I can share the code if you need it, its a little messy tbh.

1 Like