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):
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
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:
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.