So I'm much better at starting things than I am at finishing them, so while development on Crystal Daze ticks along, I wanted to try out some other ideas in Pulp. One of those builds on the use of the accelerometer as an experimental input like in Lockspinner, but typing out accelerometer is hard so I'll just call it "tilty" sometimes.
Rootin Tootin Tiltin Shootin is an idea for an ongoing game that I can release and update in stages. The basic idea is to tilt the Playdate to control a crosshair on screen, and build up a set of shooting-style minigames that can be played through. (Caveat: The key word here is "experimental". I'm seeing this project in the manner of old lightgun-style technology which was fantastic when it worked, but often somewhat dubious when ported to newer technology such as Wiimotes. Sadly there's no "MotionPlus" adapter for the PD, yet, so let's expect some fairly iffy physics here...)
Hopefully this devlog won't just cover pulp and the tilty, but also my (amateur) approach to game design and (more professional) code structure in general.
Step 1. The Thinky Bit
All my favourite ideas start out away from the screen, and the challenge of "could you do this in Pulp?" is a question I tend to come back to when I'm daydreaming.
I wanted to try using tilty as an input mechanism, but the most obvious application of rolling a ball around was too obvious. (Check out MAZE for a fine implementation of this though.) I wanted something a bit more VR like, but also wanted to keep a fairly "tight" relationship between moving the PD, and moving something on screen - that is, a natural control feel where your movement on screen reflects your movement of the device, rather than the slower, more momentum-based approach of Maze, Super Monkey Ball, etc. A crosshair seemed like a good fit, so I went with this.
Two bits of Pulp were important for this:
- The accelerometer (tilty) code, obvs.
- Decimal drawing to get the crosshair to move pixel-by-pixel, instead of tile-by-tile.
Also I figured I would need:
- Maths.
In my head, it seemed feasible. Time to fire up Pulp.
Step 2. Basic Setup
With experimental stuff, I tend to start with the most basic setup possible, in order to test things out. On this project, that came down to:
- The
player
tile, which handles the input from the tilty and drawing the crosshair, as well as any other inputs such as shooting. - The
room
tile, or "range" which sets up what the player can see, and then handles the effects of any player events.
For future expansion (ie different rooms for different stages), I want to set up a bunch of generic player code, but the player should not know anything about the room it's in. And conversely, the room should not care about what the player is doing, as much as possible.
But that's thinking ahead. For now, I just wanted to see if the building blocks could work or not.
First up, I just played around a bit with what the accelerometer events reported. For this, I dug out the old accelerometer info tool I'd put together before and ran it on device. This is really important, because a) the pulp and SDK simulators don't offer a complete range of device movements AFAIK - 3D space vs 2D space blah blah blah, and b) I really struggle to understand how the simulated Accelerometer control maps to real-world orientation anyway.
Looking at the info on device, I realised one important thing - that the forwards/backward tilt was really important. If the device is being held up, in front of your face, then tilting around the y axis (by moving your left/right hands forwards or back) is effectively ignored.
In other words, you need to tilt the screen away in order to get meaningful controls. (This is why you have to calibrate your Wiimote by putting it on a level surface all the time.) But you don't need to assume that the device is flat, in the way that a ball-rolling game equates the level of the device to the level of the floor - here, I wanted to sacrifice a little bit of accelerometer accuracy to feel like you're raising the screen up and looking ahead.
Accelerometer ranges measure from -1 to 1 but that's not useful - we want to keep the screen in view while playing! Using the tool to get an initial position for the centre of the screen, and to measure a suitable range of movement in each direction, I set up some basic variables to use in our code, along with some quick calculations to set our central pixel on screen. These could also be hardcoded easily, but hey, we only have to do it once and computers are good at these things.
// Player script:
on load do
// Screen width is 25 tiles = 200px
// Screen height is 13 tiles = 104px
// Set player to centre. Note px and py refer to the centre spot - we draw
// the target _around_ this, not from it, later on.
centre_x = 25
centre_x *= 8
centre_x /= 2 // = 100
centre_y = 15
centre_y *= 8
centre_y /= 2 // = 60
// Accelerometer position that device is in to centre the crosshair.
// In future, we might allow these to be calibrated, but let's keep it simple for now
ax_centre = 0
ay_centre = 0.7
// az_centre = 0 // not needed
// Now we set up our "field of play" - these map up/down/left/right movement to the screen edges
// The ax movement range will run from -0.3 to 0 (centre) to 0.3
// The ay movement will run from 0.5 to 0.7 (centre) to 0.9
ax_range_right = 0.3
ay_range_top = 0.9
end
In other words, for each tilty axis X and Y, we have a range of values reported by Pulp's events (event.ax
and event.ay
) and a number of pixels on screen, and we want to map the ax/ay inputs to pixel position on screen.
Using some quick maths we can then work out the relative ranges for reference. This is less useful for ax, which uses the tilty centre as the screen centre anyway. But can be useful for ay, which is offset as we're tilting the screen towards us initially. (I'm pretty sure some of this code is unnecessary, but it took a few goes to get it to work, and I'm reluctant to fix things if they're not broken )
// Still in player's load event:
// Now we can calculate the relationship between the centre pixel and the ax/ay axiseses
ax_range = ax_range_right
ax_range -= ax_centre // = 0.3
ay_range = ay_range_top
ay_range -= ay_centre // = 0.2
At this point, we just have a bunch of centres and ranges set up. We still need to actually do something useful. We'll need a quick player icon to move around, which is easy. A quick double circle will do for now, noting the transparency.
For digital drawing, we'll also need a totally transparent player tile, to cover up previous draws.
Step 3. Actual movement
Next I just wanted to see if the idea would work, so I hooked up the code to check accelerometer data to the player's draw
event. This took a few goes, because I was playing with which direction felt more natural for the x-axis tilting, and because there's a lot of conversion going on between tiles and pixels, between accelerometer ranges and screen ranges, and between absolute and relative offsets. The rough approach is:
-
Convert the actual ax/ay data into a percentage of our maximum ranges worked out above. eg tilting the screen three-quarters of our maximum range needs to be turned into 0.75. If the tilt for our maximum range in one direction is 0.3, and the event data is reading 0.2, that gets converted to 0.6666 (ie. 0.2 / 0.3). For the y axis , this is slightly more complicated as we have to remove our "centre" point first.
-
Apply these percentages to calculate the number of pixels, based on our centrepoint. For example, if we should be two-thirds in the right of the screen, we need to calculate (0.6666 * 100 = 66.66) as 100 is half the screen, and then add this to the centre (again, 100). When the accelerometer tilts to the left, two-thirds will be negative, so the calculation becomes ((-0.6666 * 100 = -66.66) + 100).
-
Then we need to take off 4 pixels (half a tile) to account for our actual crosshair point being in the middle of our player tile, and convert our pixel figures to tile-based numbers so that
draw
works properly, which just means dividing by 8.
Or, as the code ends up:
on draw do
hide
// map accelerometer to target position
// to do this, we compare the ax/ay figure with the range, eg if our maximum ax
// to the right is 0.3, event.ax = 0.3 would be 100%, or 1.0 and event.ax = 0.15
// would be 50%, or 0.5.
ax_offset = event.ax
ax_offset -= ax_centre
ax_offset /= ax_range
ay_offset = event.ay
ay_offset -= ay_centre
ay_offset /= ay_range
// Now we need to flip the ay axis
// Actually, no, keep it like ax - like a ball rolling
// ay_offset *= -1
// now calibrated to our intended ranges set on load
// Convert this into screen x and y for the target centre
// This is the same as where the original centre was
target_centre_x = ax_offset
target_centre_x *= centre_x // This might be negative if left of centre
target_centre_x += centre_x // Add or subtract from the centre point
target_centre_y = ay_offset
target_centre_y *= centre_y // This might be negative as above
target_centre_y += centre_y // Add or subtract from the centre point
// Now remove 4 for the actual drawing position
target_draw_x = floor target_centre_x
target_draw_x -= 4
target_draw_y = floor target_centre_y
target_draw_y -= 4
target_draw_pixels_x = target_draw_x
target_draw_pixels_y = target_draw_y
// Finally convert pixels to tiles
target_draw_x /= 8
target_draw_y /= 8
draw "player" at target_draw_x,target_draw_y
end
Hooray, we have something we can test! Running it up in Pulp shows that it does, in theory work:
Loading it up on device gives us something that does, technically work, but now we start to see some of the challenges in doing experimental work. In the real world, it turns out that the accelerometer info is as jittery as hell:
But I'm pleased with the progress as a proof of concept - we have device movement mapped to screen movement!
Step 4. A few tidies
The other issue is that there's nothing stopping us going off the screen. Let's address that first by just checking for some minimum and maximum pixels, remembering that our maximum needs to be one tile less than the overall screen, to keep space for the tile to be drawn.
// In player's load event just for convenience, but you could skip this and hardcode values later if you wanted
// Max x and y in pixels = number of tiles * 8
max_x = 24 // last tile - 1, to keep player tile on screen
max_x *= 8
max_y = 14 // as above
max_y *= 8
// In player's draw event, before setting target_draw_pixels_x and target_draw_pixels_y
// now check for being outside limits of screen
if target_draw_x<min_x then
target_draw_x = min_x
elseif target_draw_x>max_x then
target_draw_x = max_x
end
if target_draw_y<min_y then
target_draw_y = min_y
elseif target_draw_y>max_y then
target_draw_y = max_y
end
Better.
Then, to add in some tidying up of the digital drawing, we just draw the transparent player tile (called "player_blank") over the 4 tiles touching the crosshair where it was last drawn:
// At the top of player's draw event, before anything else happens
// remove old draw
target_draw_x = floor target_draw_x
target_draw_y = floor target_draw_y
draw "player_blank" at target_draw_x,target_draw_y
target_draw_x += 1
draw "player_blank" at target_draw_x,target_draw_y
target_draw_y += 1
draw "player_blank" at target_draw_x,target_draw_y
target_draw_x -= 1
draw "player_blank" at target_draw_x,target_draw_y
Better still.
OK, that's enough for one session. Next time we'll clear up the jittering, and start to add in something to shoot at...