Saturday, August 18, 2012
Petit BASIC Platformer Engine
Here's a platformer engine I'm working on in Petit BASIC, with in-depth comments for people who are trying to figure this kind of stuff out.
Just clearing the screen and memory so everything's nice and tidy.
(PX,PY) is the player's position. PXV and PYV are the player's current velocity in the X and Y directions, respectively. All velocities in this code are in terms of pixels per frame. Positive velocities are to the right and down, negative velocities are to the left and up.
These are the coordinates of the camera.
Here's where we keep the map, 128 by 128 tiles. I want to eventually make a roguelike sort of platformer in the vein of Spelunky, so I want my levels to have a fair amount of both length and depth. Also, I'm working with 8x8 tiles, a quarter of the size of the player's sprite. If you're thinking of making a game more like Mario, with levels that have more length than depth and 16x16 blocks, you'll need to make some adaptations.
Setting up the player character. Note the SPHOME command; this moves the player sprite's "reference point" from the top left to the bottom left. I thought this would be useful when I started, but now I'm not so sure. All of my calculations are based on this idea right now, but I may change it in the next version.
FOR I=0 TO 10
FOR J=22 TO 23
FOR I=0 TO 60
FOR I=25 TO 31
FOR I=5 TO 18
FOR I=0 TO 127
This whole block is just filling the map up with some basic shapes so you have something to play around on and see how the engine works. In a real game, this would either be replaced with a data-reading loop or some sort of level generation algorithm.
44, of course, is the number of the character tile that I'm using for ground.
FOR I=0 TO 32
FOR J=0 TO 24
Before we begin the main loop, we fill the screen with data from the MAP array.
Display the player character.
Update the player's position based on his velocities. Also, change the Y velocity due to gravity (.1 pixel/frame^2).
IF PYV>2 THEN PYV=2
Terminal velocity. The player won't fall faster than 2 pixels per second.
Now we're going to check for vertical collisions with the background. CY is true if we're clear in the Y direction. GR is true if the player is "grounded"; that is, not falling.
(IX1, IY1) and (IX2, IY2) are the tiles at the corners of the rectangle that the player overlaps. This tells us which tiles to check.
J=IY1:IF PYV<0 j="IY2</p" then="then">
If the player is rising, we check the tiles above, otherwise, we check the tiles below.
FOR I=IX1 TO IX2
IF B>0 THEN CY=FALSE
BGREAD gives us information about background tiles, but the only one we're concerned with is B, the character number of that tile. In this engine, everything except 0 is a wall that the player can't cross. We'll need to change this if we add other screen elements that the player can cross.
IF CY THEN @HORIZ
IF PYV<0 else="else" then="then" y="(IY1*8)-1:GR=TRUE</p">PYV=2
If we encountered a block in our check, then we bump the player back to the outer edge of the block. If the player was falling as this happened, then we also consider him to be grounded; it will be important to know this when we want to decide whether or not to let the player jump.
Note also that all vertical collisions change the Y velocity back to 2. In other words, if you bump your head in the middle of a jump, you fall right away.
Now that we're sure of the player's Y position, we update it and get ready to check for horizontal collisions.
I=IX1:IF PXV0 THEN I=IX2
FOR J=IY2 TO IY1
IF B>0 THEN CX=FALSE
IF CX==FALSE THEN X=IX*8:PXV=0
This is the same basic idea as the vertical collision detection, except checking X values. Note that all X collisions result in an X velocity of 0; the player stops short.
At this point, we check to see if the screen scrolls. I decided to make this its own subroutine.
Now we're ready to read the controls. Here, I capture the state of the buttons.
IF BU AND 8 THEN PXV=PXV+.2
IF BU AND 4 THEN PXV=PXV-.2
If the player is pushing left or right, the player's velocity increases in that direction.
IF (BT AND 32)==32 AND GR THEN PYV=-2.25:BEEP 8,-4096
I prefer the Super Mario World controls (Y=Run, B=Jump) on the DS, so B is our jump button. We make sure the player is pressing the button AND that the player is currently grounded; if we just checked the button, the player could jump in midair. Also, notice that I'm using the BTRIG value to check the button state. We only want the player to jump at the moment he presses the button; if we used the BUTTON value, the player could "bunny hop" by holding the B button down.
To make the player jump, we just give him a negative Y velocity (in physics, an "impulse"), and BEEP the jumping noise.
L=1:IF PXV<0 l="-1</p" then="then">IF BU AND 128 THEN L=L*2
IF ABS(PXV)>ABS(L) THEN PXV=L
Checking the player's horizontal velocity. Ordinarily, the player has a maximum velocity of 1 pixel per frame, either left or right. But if the run button, Y, is held down, that's doubled.
IF PXV>0 THEN PXV=PXV-.1
IF PXV<0 pxv="PXV+.1</p" then="then">
Friction. The player slows down unless he's holding down a direction button.
IF ABS(PXV)<.1 THEN PXV=0
Petit BASIC has imprecise fractional math; I found it necessary to detect small fractions and kill them.
Wait for the next frame, then do it all again!
Next up, we're going to look at how the screen scrolls. This is a little complicated. It helps to know, before you begin, how the background in Petit BASIC is represented.
First, think of the background as a grid of tiles, 64x64, with X and Y coordinates numbered 0 to 63. Each tile in this grid is 8 pixels by 8 pixels, so you can also think of the background as a grid of pixels, 512x512.
The screen only shows a small part of this background, 32 tiles horizontally and 24 vertically. So we also have a "camera" which we can move around to show different parts of the screen. The camera can move pixel by pixel, so the screen isn't always going to be exactly lined up with the background tiles. If you think of the background as a grid of pixels, then the camera's "offset" would be the coordinates of the top left pixel of the screen relative to the background.
Once you've got your head around that idea, you should understand that the background wraps around, both horizontally and vertically. This means that when you refer to background tile (64,64), you will actually be referring to tile (0,0). Likewise, camera offset (512,512) is the same as (0,0).
This is incredibly valuable. It means that we can treat the background as if it was ridiculously long in all directions. Take our map for example. We want to tell the system "Put the character from MAP(X,Y) onto tile (X,Y) of the background." As we scroll to the right, X goes from 0 to 127. When X is from 0 to 63, we write to the background X from 0 to 63. When X is 64, we just keep scrolling, but now we're writing to the background X starting at 0 again. This works because, by this point, we're not displaying what we originally had at background X 0 anymore; it's already scrolled off to the left.
The basic strategy is this:
Start by drawing the initial state of the screen.
When it's time to scroll, assume that the tiles you're about to scroll into are garbage and overwrite them with the correct values.
As long as you follow those two rules, you can scroll the screen out for a very, very long time. (You'll eventually get overflow errors when X and Y get out to about 500,000 or so; luckily, this program doesn't get up that high.)
First, figure out where, in the tile grid, the camera is currently pointing. XOFS and YOFS are, as I said before, the offset position of the camera.
IF PX<=136 OR XOFS>767 THEN @S2
First, we check to see if we need to scroll the screen to the right. I've decided that the scroll zone should be around the middle of the screen, but there's a little leeway so that the player can go back and forth without the camera moving. So the first condition is to see if the player has entered the "scroll zone".
The other condition is that we don't want the camera to move if the player has reached the edge of the map. I've calculated 768 to be the point where the camera stops.
We find out how many pixels past the scroll zone the player is. The camera moves that many pixels to the right, and the player moves that many pixels back to the left.
IF XOFS>768 THEN XOFS=768
If we accidentally moved the camera too far, we bump it back into place.
IF FLOOR(XOFS/8)<=MX THEN @S2
Now we check to see if the camera has gone past the last set of tiles that we've drawn. If it has, then we have to draw them in.
FOR I=0 TO 24
This is just drawing the next line of tiles onto the background. Notice the modulo (%) math here. When we get to the edge of the map, MX+33 and MY+I may become larger than 127, which is the largest value we can refer to in our array. The modulo effectively makes these values "wrap around"; 128 becomes 0, 129 becomes 1, and so on. This does mean that the left and top edges of the level will be redrawn along the right and bottom edges, but since the camera stops before those are displayed, the player will never know! It's basically just a quick trick to keep the system from throwing an error.
IF PX>=120 OR XOFS<1 p="p" then="then">DX=FLOOR(120-PX):XOFS=XOFS-DX:PX=PX+DX
IF XOFS<0 then="then" xofs="0</p">IF FLOOR(XOFS/8)>=MX THEN @S3
FOR I=0 TO 24
Same principle, scrolling left. Maybe the redundancies here are a little wasteful in terms of the size of code. I might have to think about how this can be improved in the next version.
IF PY<=164 OR YOFS>831 THEN @S4
IF YOFS>832 THEN YOFS=832
IF FLOOR(YOFS/8)<=MY THEN @S4
FOR I=0 TO 32
Same principle again, scrolling down.
IF PY>=66 OR YOFS<1 p="p" then="then">DY=FLOOR(66-PY):YOFS=YOFS-DY:PY=PY+DY
IF YOFS<0 then="then" yofs="0</p">IF FLOOR(YOFS/8)>=MY THEN @S5
FOR I=0 TO 32
Same idea again, scrolling up.
Finally, we move the camera to its new position, and we're done!
This is a pretty simple engine, just a player and some fixed blocks. What I'd like to do next is generalize the physics a bit and make arrays for all of the positions and velocities, so that the player is just one possible object in a larger system. But I wanted to post this, with comments, because it's still relatively simple; people can look at all of the pieces and have an idea of how they work without also having to think in terms of OBJECT(X) or whatever. And, of course, there's animation to add and all that good stuff.
So there you go. That's where I am.