Implementation of limited sight mechanic in pulp

There were a lot of discussions on this topic in different forums and on Discord, so i decided to summarize here and add my own two cents.

TL:DR - You can implement a fun limited sight mechanic

player_lighting_tricks

Here's the pdx and Pulp json for anyone who wants to take a look:
Player_lighting_tricks.zip (51.8 KB)

I was chatting with Guv Bubbs about his pulp light game that he's developing that has dark stages that are illuminated by the player's lantern. His prototype looked fantastic, but unfortunately suffers from performance issues when running on the Playdate Hardware. He was trying to use the fill function, which as Shaun and Neven are concstantly reminding us needs to be used very sparingly :sweat_smile:

What he was trying to accomplish reminded me of the mechanic that JeyB and Laura Hall used in their excellent game Your Turn Little Bot that they made for the Pulp game jam. In addition to the great game, they were also kind enough to provide their code, so i'd definitely recommend that you check it out!

Anyway, i thought I'd share the approach that the used that they came up with and i put together in a demo based on everyone's recommendations. I guess the best way to talk about it is the lessons learned in a series of bullet points:

  1. Avoid copious use of fill. It's very process intensive on the hardware and should be used sparingly!
    Instead, JeyB

  2. Avoid using tons of code in the Player's draw event handler, since it's called every frame that the game is running.

To quote Pulp Guru Shaun: "A good rule of thumb with PulpScript is "do less, less often."

A better way to approach this would be to play to Pulp’s strengths: tiles. For single frame tiles, switch between a light and dark frame as they pass in and out of the ring of light. For animated tiles, swap between light and dark versions. These tiles will only be redrawn when they change. Also, making this tile-based allows you to add highlights to dark frames, eg. to provide landmarks in a dark room."

The solution that everyone came up with is to manipulate the frames of the tiles that we want to swap between "light" and "dark". We make frame 0 (the first frame) the normal, "light" version, and the frame 1 (the second frame) the dark version, which can either be total black, or something shadowy to catch the player's eye.

(setting the fps to 0 is necessary for us to be able to disable auto-animation and let us set the frame manually in our PulpScript)

Now for any room that we want to darken, and then illuminate around the player, we can use this PulpScript:

on enter do
	room_illuminated = 0
	tell event.game to
		call "darken_room"
		call "illuminate_tiles"
	end
end

The first function, as the name suggests, simply iterates over all of the tiles and sets the frame to 1 (the frame with the tile's "dark" version). The second function will then look at where the player is, and illuminate the tiles in a radius around him by changing the tiles' frame back to 0.

This is a good amount of operations, but it should be ok since it's just a one-off operation that we're doing when the player enters a dark room.

But since we're using the tiles' frames to manage all this, we can't use the frames to have our tiles be animated :frowning:

... unless we come up with a clever workaround :slight_smile:
In our case, we can create 2 different versions of the tile. In our illuminate_tiles function, we're setting the frame to 0 and also calling the tile's "brighten_tile" function:

	// illuminate tiles around player location by swapping to frame 0
	while _x<=x_max do
		while _y<=y_max do
			tell _x,_y to
				frame 0
				// tell tile to swap to its "bright" version if it has one
				call "brighten_tile"
			end
			_y++
		end
		_x++
		_y = y_min
	end

If the tile we're illuminating doesn't have a "brighten_tile" function then nothing happens. But if it does have it, it'll be called and we can do something. In our case we want the tile to swap out for the "bright" version that can have a fun animation to show. Take a look at this bat sprite:
CleanShot 2022-02-04 at 18.05.50
Here we can see the "light" version. Its PulpScript will swap it out for the dark version either when the player enters the room, or when the darken_tile function is called, for example when the player walks away and we want that part of the stage to go dark again

on darken_tile do
	swap "bat_dark"
end

on enter do
	swap "bat_dark"
end

If we take a look at the dark version, we can see that it has the "brighten_tile" function that we can call to swap to the light version. Note that neither of these tiles have the fps set to 0, so they'll just play their animations normally. Even if our code is telling it to set the frame to 1 or 0, it'll ignore this command since fps isn't set to zero.

Now let's see it in action!
lighting_tricks

OK, now the critical part of all of this: How we handle this in the Player's PulpScript to avoid performance problems. JeyB and Laura figured out while they were making their game that updating all of these tiles was pretty computationally costly for the Hardware, so the trick is to only update the first and last column (or row) where the player is walking.

We only wan to call this when the player moves (not every frame), so in the player's update event handler we have:

	// if the room isn't flagged as illuminated then update the tiles that we're going to illuminate
	if room_illuminated==0 then
		call "update_illuminated_tiles"
	end

then

on update_illuminated_tiles do
	// since we're calling this more often, we'll only update the tiles that are changing when the playe rmoves
	x_min = event.px
	x_min -= illumination_radius
	x_max = event.px
	x_max += illumination_radius
	
	y_min = event.py
	y_min -= illumination_radius
	y_max = event.py
	y_max += illumination_radius
	
	// make sure we're not trying to access tiles off-screen
	while x_min<0 do
		x_min++
	end
	while x_max>24 do
		x_max--
	end
	
	while y_min<0 do
		y_min++
	end
	while y_max>14 do
		y_max--
	end
	
	_x = x_min
	_y = y_min
	
	
	// illuminate tiles where player is walking and re-darken tiles now outside of the illumination radius
	// NOTE: to make this as few operations as possible, we're only going to update the tiles at the edge of the radius that need to be changed
	if event.dx>0 then
		// player is moving right, so re-darken tiles to the left
		darken_x = x_min
		if darken_x>0 then
			darken_x--
		end
		
		while _y<=y_max do
			// illuminate tiles ahead of player
			tell x_max,_y to
				frame 0 // frame zero is "bright" version of tile
				// tell tile to run its animation if it has one
				call "brighten_tile"
			end
			// re-darken tiles now outside of the illumination radius
			tell darken_x,_y to
				call "darken_tile" // if defined in tile, will swap back to the darked version
				frame 1 // frame 1 is "dark" version of the tile
			end
			_y++
		end
		
	elseif event.dx<0 then
		// player is moving left
		darken_x = x_max
		if darken_x<24 then
			darken_x++
		end
		
		while _y<=y_max do
			// illuminate tiles ahead of player
			tell x_min,_y to
				frame 0 // frame zero is "bright" version of tile
				call "brighten_tile" // if defined in sprite, will swap to bright version
			end
			// re-darken tiles now outside of the illumination radius
			tell darken_x,_y to
				call "darken_tile" // if defined in the sprite, will swap back to the darked version
				frame 1 // frame 1 is "dark" version of the tile
			end
			_y++
		end
		
	elseif event.dy<0 then
		// player is moving up
		darken_y = y_max
		if darken_y<14 then
			darken_y++
		end
		while _x<=x_max do
			// illuminate tiles ahead of player
			tell _x,y_min to
				frame 0 // frame zero is "bright" version of tile
				call "brighten_tile"
			end
			// re-darken tiles now outside of the illumination radius
			tell _x,darken_y to
				call "darken_tile" // if defined in tile, will swap back to the darked version
				frame 1 // frame 1 is "dark" version of the tile
			end
			_x++
		end
		
	elseif event.dy>0 then
		// player is moving down
		darken_y = y_min
		if darken_y>0 then
			darken_y--
		end
		while _x<=x_max do
			// illuminate tiles ahead of player
			tell _x,y_max to
				frame 0 // frame zero is "bright" version of tile
				call "brighten_tile"
			end
			// re-darken tiles now outside of the illumination radius
			tell _x,darken_y to
				call "darken_tile" // if defined in tile, will swap back to the darked version
				frame 1 // frame 1 is "dark" version of the tile
			end
			_x++
		end
		
	end
	
end

And there you have it! Take a look at the attached code for more nitty gritty details, and hopefully you found this post illuminating!

9 Likes

Thanks for...shedding some light on this topic. I’m curious about this chunk:

	while x_min<0 do
		x_min++
	end
	while x_max>24 do
		x_max--
	end
	
	while y_min<0 do
		y_min++
	end
	while y_max>14 do
		y_max--
	end

Why not just use if/elseif? eg.

	if x_min<0 then
		x_min = 0
	elseif x_max>24 then
		x_max = 24
	end
	
	if y_min<0 then
		y_min = 0
	elseif y_max>14 then
		y_max = 14
	end
1 Like

Glad our game helped you figure this out! And congrats on parsing through my jamcode :grinning:

2 Likes

Is it better to use if over while?

lol, you're totally right :joy:. I'm not sure what I was thinking when i coded that up, haha. @Guv_Bubbs, it's better to use just because it's fewer operations, and the fewer operations you have the less likely you are to run into performance problems.

"A good rule of thumb with PulpScript is "do less, less often."
-Somebody that probably knows a lot about Pulp

2 Likes