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!

10 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
2 Likes

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

3 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

Hey,

Thanks so much for sharing this! I am a complete newbie to Pulp and coding though and was wondering where this code actually goes. Do I need to do a lot of my own re-writing, or are there sections I can cut and past into my own game?

Thank you!

I'll like to revive the contributors in this thread if possible for a bit of help in how I've implemented this mechanic into my most recent game Pine. Help!

Everything works fine except for some performance issues (on the actual Playdate device) when the player-character (PC) triggers certain animated sequences (i.e. falling down a mineshaft when stepping onto a tile representing that open mineshaft).

I suspect its all the calling of event handlers that deal with turning on and off (swapping) the tiles as the PC triggers sequences that move faster than simply walking around a tile at a time in a dark space.

I'm wondering if anyone could take a look at my code (potentially sending you the .json) to see how I could tidy this up for smoother performance for players? There are no crashes, but it aint smooth either.

It first appears when falling down into the room "037_pine_island_below_mine"

Basically, the existing code is meant to illuminate an area one tile around the PC to simulate a light source which moves with the PC. However, the existing code is causing a lag in the graphics and sound on device. We can see the lag when moving to 21, 11 and a fall down is triggered:

on room_update do

//
// minefall 1
    if player_pos==2111 then
        ignore
        once "ShortFall"
        tell event.game to
            call "darken_room"
        end
        player_rep = 5
        tell event.player to
            swap "player_right_minefall"

            wait 0.1 then
                goto 21,12

                tell event.player to
                    call "update_illuminated_tiles"
                end

                wait 0.1 then
                    goto 21,13

                    sound "FallCrash"

                    tell event.player to
                        call "update_illuminated_tiles"
                    end

                    wait 0.1 then

                        tell event.player to
                            play "player_right_wake_mine"

                            wait 5 then
                                tell event.player to
                                    swap "player_right"

                                    if health_warning==0 then
                                        say "    This last\n   fall leaves\n   you limping." at 3,1
                                        loop "The Mines"
                                        player_speed = 1
                                        health_warning = 1
                                        player_rep = 0
                                    end
                                    listen
                                end
                            end
                        end
                    end
                end
            end
        end
    end

The suspects are:

  1. tell event.game to
    call "darken_room"
    end

  2. tell event.game to
    call "illuminate_tiles"
    end

  3. tell event.player to
    call "update_illuminated_tiles"
    end

Nevermind-- I've got this about worked out.