Devlog: Rootin Tootin Tiltin Shootin, a Pulp-based tilty gun game

Time to move on. This time, we can add a few more pieces of the puzzle and - very loosely - maybe start to call this a "game".

Step 5. Smoother movement.

We got movement working last time, but the Playdate's accelerometer ("tilty") is fairly inaccurate and the crosshair ends up jumping around the place a fair bit. We'll smooth it out by averaging the readings over a few cycles.

Got to be a bit careful here as we're translating between tilty data (event.ax and .ay), pixels, and tile measurements, so it's easy to get confused. I want to average out the raw tilty data just to get the hard work out the way and not have to think about it later in the code.

I'm going to average the last 5 frames out, which is pretty arbitrary but a good starting point. Pulp doesn't do arrays, so this means I'll need to take the average of 5 sets of x/y variables. (I could set up 4, and transfer event.ax/ay after taking an average, but I'll keep all the code together for cleanliness).

I also thought about moving the latest reading into set 5, after moving the previous set 5 into 4, 4 into 3, etc. But that seems inefficient, so instead I just set up a single "pointer" variable to move to the next set each frame.

When the player enters the room:

	// Our counter for storing current accelerometer readings in our samples "array"
	crosshair_sample = 0
	
	// Set up all samples for averaging, otherwise we'll start by averaging the
	// current position with 3 zeros
	crosshair_sample_1_x = event.ax
	crosshair_sample_1_y = event.ay
	
	crosshair_sample_2_x = event.ax
	crosshair_sample_2_y = event.ay
	
	crosshair_sample_3_x = event.ax
	crosshair_sample_3_y = event.ay
	
	crosshair_sample_4_x = event.ax
	crosshair_sample_4_y = event.ay
	
	crosshair_sample_5_x = event.ax
	crosshair_sample_5_y = event.ay

On the player's draw event, we "intercept" event.ax/ay, store them in one of our 5 variable sets, and take an average by adding them and dividing by 5:


	// Now get the current accelerometer reading
	ax_offset = event.ax
	ay_offset = event.ay
	
	// v0.2 - average out over a couple of frames
	crosshair_sample += 1
	if crosshair_sample==1 then
		crosshair_sample_1_x = ax_offset
		crosshair_sample_1_y = ay_offset
	elseif crosshair_sample==2 then
		crosshair_sample_2_x = ax_offset
		crosshair_sample_2_y = ay_offset
	elseif crosshair_sample==3 then
		crosshair_sample_3_x = ax_offset
		crosshair_sample_3_y = ay_offset
	elseif crosshair_sample==4 then
		crosshair_sample_4_x = ax_offset
		crosshair_sample_4_y = ay_offset
	elseif crosshair_sample==5 then
		crosshair_sample_5_x = ax_offset
		crosshair_sample_5_y = ay_offset
		// And reset the loop
		crosshair_sample = 0
	end
	
	// Calculate average from this and last 3 frames
	ax_offset = crosshair_sample_1_x
	ax_offset += crosshair_sample_2_x
	ax_offset += crosshair_sample_3_x
	ax_offset += crosshair_sample_4_x
	ax_offset += crosshair_sample_5_x
	ax_offset /= 5
	
	ay_offset = crosshair_sample_1_y
	ay_offset += crosshair_sample_2_y
	ay_offset += crosshair_sample_3_y
	ay_offset += crosshair_sample_4_y
	ay_offset += crosshair_sample_5_y
	ay_offset /= 5
	

(Side note: Understanding how averages work is one of the most useful things you can ever learn.)

This gives us something that feels less jumpy, at least (captured using device control in the Simulator):

rrts-devlog-5

Step 6. Setting up targets

An empty shooting gallery is pretty dull, so we need something to point our newly developed pistol / laser beam / magic wand at. What could be be less simple than a box? The plan is just to generate a random box on screen, set up code to handle the player pressing the "A" button, and increase a score variable if the cursor was inside the box at the time the button was pressed.

As mentioned before, I like to separate code out into tiles like I would classes in Object-Oriented Programming. (Tile-Oriented PulpScript, or TOPS?) In other words, the player should store the code to do with how the player acts, and the room should store the code to do with how the room acts, and they should communicate between themselves using custom events.

So the room, called "range" is responsible for setting up targets, handling how shots are received, and keeping track of score. Let's leave scoring aside for the moment. To set up targets, we'll need variables to track:

  • How long to wait between one target being shot, and the next being shown
  • Whether a target is being shown
  • The position and size of a target (x and y for top, bottom, left and right sides)
  • The maximum and minimum possible sizes for a target, and the range on the screen in which to show them

I set these up when the player enters the room, so that I can set up different things when I get round to adding other rooms.

In my "range" room:

on initRoom do
	// Is there a target o screen?
	target_shown = 0
	
	// Where is the target being shown?
	target_xt = 0
	target_yt = 0
	target_xb = 0
	target_yb = 0
	
	// How long til the next target gets shown?
	// Keep this consistent for now, to not be a factor in scores
	target_countdown = 3
	
	// These need to be inset slightly as the centre of the target can't be at
	// the edge of the screen
	target_min_x = 5
	target_min_y = 5
	
	target_min_w = 20
	target_min_h = 20
	target_max_w = 50
	target_max_h = 50
	target_w_range = target_max_w
	target_w_range -= target_min_w
	target_h_range = target_max_h
	target_h_range -= target_min_h
	
	target_max_x = 200
	target_max_x -= target_min_x // Make sure we don't go beyond the space that the centre of the target can't reach
	target_max_x -= target_max_w
	target_max_y = 120
	target_max_y -= target_min_y
	target_max_y -= target_max_h
	end

This then gets called from the enter event:

on enter do
  call "initRoom"

As I plan to use the fill command to show the target box for now, rather than using swap command to alter the tiles between frames (one for later), the target_shown variable is needed. Other rooms will set this all up differently.

Then we need a custom event to create a new random target box, and tell the room to show it:

on showTarget do
	target_x = random target_max_x
	target_y = random target_max_y
	target_w = random target_w_range
	target_w += target_min_w
	target_h = random target_w_range
	target_h += target_min_h
	
	target_x_right = target_x
	target_x_right += target_w
	target_y_bottom = target_y
	target_y_bottom += target_h
	
	target_shown = 1
end

This doesn't actually show it. To do that, we need to check target_shown when drawing the screen, and use fill. The draw event is called on the Player, so I tend to use a custom player_draw event to ask the current room to do anything at draw time:

In player's script:

on draw do
    ...	
	tell event.room to
		call "player_draw"
	end
	...

Then, in the room's script, we can finally draw our box:

on player_draw do
	if target_shown==1 then
		fill "black" at target_x,target_y,target_w,target_h
	end
end

This covers generating a random box target and showing it, but you'll notice we haven't called the showTarget event above yet. To do this the first time, we just have to wait the number of seconds set in target_countdown after entering the room. I put this into a separate custom event though, so that we can re-use it later:

on resetTargetCountdown do
	wait target_countdown then
		call "showTarget"
	end
end

And on enter becomes:

on enter do
	call "initRoom"
	call "resetTargetCountdown"
end

rrts-devlog-6

Step 7. Some actual rootin tootin shootin

Last bit for now. We have all the pieces in place, we just need to check the cursor's midpoint when the player presses A, increase a score if it was in the right place, and trigger a new target box.

First I'll set up a score variable. As the scoring system might change for each room, I'll just do this in the initRoom event.

  score = 0

Like the player passing the draw event to the room as player_draw above, I normally like to set up a player_confirm event as well. As this is a dedicated shooting gallery though, I'm renaming it to playerFired. This sets up some "parameter" variables (starring with p_) for the script that catches it, based on the crosshair's current position. For now, I'm converting the tile-based position back into pixels, but I could optimise this by re-using the pixel position from the draw event.

In player's script:

on confirm do
	p_crosshair_x_pixels = crosshair_draw_x
	p_crosshair_x_pixels *= 8
	p_crosshair_x_pixels += 4
	p_crosshair_y_pixels = crosshair_draw_y
	p_crosshair_y_pixels *= 8
	p_crosshair_y_pixels += 4
	tell event.room to
		call "playerFired"
	end
end

The room script then deals with this by playing a sound effect called "shot", and checking the passed in pixel x/y values against the current target's position (if a target is shown). If the position is on target, then we change target_shown to 0 to stop it being drawn, increase the score by 1, and request a new target in a few seconds:

on playerFired do
	sound "shot"
	
	if target_shown==1 then
		if p_crosshair_x_pixels>=target_x then
			if p_crosshair_x_pixels<=target_x_right then
				if p_crosshair_y_pixels>=target_y then
					if p_crosshair_y_pixels<=target_y_bottom then
						// A hit!
						target_shown = 0
						score++
						call "resetTargetCountdown"
					end
				end
			end
		end
	end
end

I then just show the current score using label in the player_draw event in the room:

label "{score}" at 0,14

Et voila:

rrts-devlog-7

Is it playable? Yes! Is it exciting and any good? Maybeeee. Is it a game? In theory, you can get someone to time you and see what score you can get in 60 seconds, so yes, it's a game! Is it catalogue-ready?

Well... Maybe it does need its own timer after all. But that's for another day.

1 Like