Thursday, August 30, 2012

 

Petit Computer Platform Engine v1.0



Here it is -- the complete, 1.0 version of my Petite Computer platform game engine.

What this is not:  A game kit.  I have not tried to make this a generic game where you draw the levels and place monsters and items.  Creating a game with this engine will take a lot of custom programming.

What this is: A head start.  I wanted to make a framework that takes care of a lot of the tedious stuff that's common to most platform games.  It gives you four-directional scrolling, gravity, moving platforms, and objects with customizable attributes.  I wanted to make it as easy as possible for someone with some programming experience to get a foot in the door and make something working without getting mired down in the early mechanical details.

This engine is intended to be sliced, diced, and rebuilt to fit your idea and make the game you want to make.

To help you better understand it, here's a complete program listing with in-depth comments to explain exactly what's happening.  This may not be the most efficient way to solve these problems, but it should give beginners some idea of the basics.

ACLS: CLEAR

Clear the screen, clear the memory.

MAPX=128:MAPY=128:OBJ=5

Petit Computer doesn't really have constants, so I'm keeping these important values in variables.  You can, of course, change their values here if your program has different needs, and there may be reasons to modify them as the program runs, but these values are used to set the dimensions of important arrays, so it's a bad idea to let them get bigger than whatever initial value you set here.

MAPX and MAPY are the size of the level map, and they also affect the constraints of the camera.  OBJ is the number of moving objects you want to have at one time.

DIM MAP(MAPX,MAPY)

Here's our level map.

DIM SX(OBJ),SY(OBJ)

The X and Y coordinates, respectively, of every object in the system.  In a previous version, these coordinates were relative to the screen, but it proved to be a lot easier to define them as being relative to the level map.  This allows us to keep track of off-screen objects and allow them to keep interacting with the level.

Note that object 0 is always the player character.

DIM SXV(OBJ),SYV(OBJ)

The X and Y velocities, respectively, of every object in the system.  Negative velocities are up and to the left, positive velocities are down and to the right.  All velocities are pixels per frame.

DIM FL(OBJ)

'Flags
'1 - Pass through walls
'2 - Ignore gravity
'4 - Turn around at walls
'8 - Turn around at cliffs
'16 - Carry the player
'32 - Stop when offscreen

The FL array is for "flags".  There are a number of special behaviors that occur a lot in platform games -- flying objects, moving platforms, and so on.  I've included the code for these behaviors, and you can enable them by setting the FL value for each object in the system.  These are bit flags, so you set the value by adding together the value for each behavior you want and use bitwise operators to see which are set for each object.  For instance, if you want a bullet that goes through walls, you'll want to pass through walls and ignore gravity: 1+2=3.

Here's a complete explanation for the six different kinds of behavior.  I'll show how they're actually implemented when we reach those points in the code.

Pass through walls
The default behavior is to check if moving a sprite causes it to overlap with a background element, and if so, to move it back away from the background element.  If this flag is set, the object will not perform these checks.  You can use this for ghost enemies, projectiles that pass through walls, and so on.

Ignore gravity
The default behavior is for every object to accelerate in the Y direction due to gravity at a rate of .1 pixels/frame^2.  If this flag is set, the acceleration isn't applied.  Objects that pass through walls will usually need this flag set too, otherwise they'll drop through the floor and out of the level.  Good for flying objects.

Turn around at walls
The default behavior is for objects to stop (X velocity set to 0) if they run into a wall.  If this flag is set, the object will instead turn around -- that is, the sign will be set so that the velocity is in the opposite direction of the wall.  This is good for unintelligent creatures that just mill around aimlessly.

Turn around at cliffs
The default behavior is for objects to move straight forward at the rate of X velocity, straight off the edge of platforms.  If this flag is set, the object will instead turn around when it reaches the end of a platform.  This is good for unintelligent creatures that just stick to a single platform and defend it.

Carry the player
If this flag is set, the object will act as a platform for the player and carry the player object as it moves around.  This is good for moving platforms.

Stop when offscreen
The default behavior is for all objects to update every frame.  If this flag is set, the object will only update its position when it is actually onscreen.  This is good for creating situations where the movement of enemies is important, and you want to make sure they're in a certain position when the player arrives; for example, an enemy that falls down from its platform to ambush the player shouldn't move until the player actually reaches that part of the level.

What follows are examples of how to set the initial positions and behaviors of four sprites.

@STARTGAME
SX(1)=200:SY(1)=160:SYV(1)=-1:FL(1)=31

Our first sprite starts at point (200,160).  It has all of the flag behavior except stopping offscreen (1+2+4+8+16=31), so it ignores gravity and walls and it can carry the player.  (The other two behaviors don't affect the way this object works because it ignores walls.)  I set an initial Y velocity because, ignoring gravity, I need to specify this myself to get it to move.

SX(2)=10:SY(2)=10:SXV(2)=1:FL(2)=20

Second sprite starts at (10,10).  It turns around at walls and carries the player.  (4+16=20)  We set an initial X velocity to start it running.

SX(3)=200:SY(3)=160:SXV(3)=1:FL(3)=8

Third sprite starts at (200,160).  Its only abnormal behavior is turning around at the edge of platforms.  (8)

SX(4)=720:SY(4)=0:SXV(4)=-1:FL(4)=36

Fourth sprite starts offscreen at (720,0).  This one turns at walls and doesn't move unless it's onscreen.

You can run the program and observe the moving objects to confirm that this is the way they behave.  Change the values of the flags to see how they can behave differently.

Note that we've only initialized sprites 1-4.  Sprite 0, the player, has no modifications.  Therefore, he'll begin at (0,0) with no unusual behavior.

XOFS=0:YOFS=0

We start the camera in the upper left corner of the level.

SPSET 0,64,0,0,0,0
SPSET 1,14,0,0,0,0
SPSET 2,14,0,0,0,0
SPSET 3,14,0,0,0,0
SPSET 4,128,0,0,0,0

Setting the characters for our four sprites.  I don't deal with animation in this demonstration because I decided that it's a tricky enough proposal that it's more useful for a programmer to custom-code it for the game he's creating than for me to try and generalize it in this engine.

FOR I=0 TO 10
FOR J=22 TO 23
MAP(I,J)=44
NEXT J
NEXT I
FOR I=0 TO 60
MAP(I,23)=44
NEXT I
FOR I=25 TO 31
MAP(I,22)=44
MAP(I,4)=44
NEXT I
FOR I=5 TO 18
MAP(O,I)=44
MAP(5,I)=44
NEXT I
FOR I=20 TO 127
MAP(I,127)=44
NEXT I
FOR I=65 TO 127
MAP(I,20)=44
NEXT I
MAP(20,20)=44

Here, I'm just filling the map with blocks, character 44.  Reading or generating level data is also left as an exercise for the programmer.

FOR I=0 TO 32
FOR J=0 TO 24
BGPUT 1,I,J,MAP(I,J),8,0,0
NEXT J
NEXT I

Before we begin the main loop, we fill the background with the first screen's worth of blocks.

@MAIN
RI=0

RI keeps track of which moving platform the player is currently riding on.  A 0 indicates none.

FOR TH=0 TO OBJ-1

We run this loop for every object in the system.  TH is short for "this", the object we're currently dealing with.

X=SX(TH):Y=SY(TH)

We put the X and Y coordinates of the current object into X and Y, mostly to make the code easier to read.

SPOFS TH,X-XOFS,Y-YOFS

Draw the sprite on the screen.  Since X and Y are relative to the overall map, we subtract the X and Y offset of the camera to find their position on the screen.

C1=((FL(TH) AND 32)==32)
C2=(ABS(X-XOFS-120)>136) OR (ABS(Y-YOFS-88)>104)
IF C1 AND C2 THEN @PHYDONE

Here, I'm breaking down the conditions of my IF statement on multiple lines.  I do this a bit to improve readability and to keep from pushing the 100 characters per line limit.  Condition 1 is true if this object doesn't update when it's offscreen.  Condition 2 is a tricky bit of math that only returns true if the X and Y coordinates fall outside the range -15 to 255 and -15 to 191, respectively -- in other words, if the object is offscreen.    If both conditions are true, we skip to the bottom of the physics loop.

Y=Y+SYV(TH)

Update the object's Y position according to its velocity.

IF FL(TH) AND 2 THEN @SKIPGRAV

If the object ignores gravity, skip gravity acceleration.

SYV(TH)=SYV(TH)+.1

Gravity acceleration, .1 pixels/frame^2.

IF SYV(TH)>2 THEN SYV(TH)=2

Terminal velocity, 2 pixels/frame.

@SKIPGRAV

CY=TRUE:G=FALSE:H=FALSE

Now we're going to check for vertical collisions with the background.  CY keeps track of whether the Y direction is clear, and it's true until we hit something.  G keeps track of whether this object has hit the ground, and H keeps track of whether this object has hit its "head".

IX1=FLOOR(X/8):IX2=FLOOR((X+15)/8)
IY1=FLOOR(Y/8):IY2=FLOOR((Y+15)/8)

(IX1,IY1) and (IX2,IY2) are the tiles at the top left and bottom right of this object, respectively.

AV=SYV(TH):IF TH==0 AND RYV!=0 THEN AV=RYV

We need to know the velocity of this object in the Y direction to decide which direction to check for collisions.  Usually, this will just be SYV(TH).  However, if the player is on a moving platform, the player's Y velocity may not be the actual direction that it's traveling.  RYV holds the Y velocity of whatever sprite the player may be riding.  So if TH=0 -- in other words, if we're doing this loop for the player object -- and RYV isn't 0 -- if the player actually is riding something -- then we use the velocity of the object the player is riding instead.

J=IY2:IF AV<0 j="IY1</p" then="then">
Based on the direction this object is moving, we check above or below for collisions.

FOR I=IX1 TO IX2
A=ABS(I%MAPX):B=ABS(J%MAPY)
IF MAP(A,B)>0 THEN CY=FALSE
NEXT I

We loop through all of the possible tiles that the object may be colliding with.  If there is a block in that position, then the Y direction isn't "clear".

Note the tricky math to determine which map element to check.  This is because an object may travel beyond the boundaries of the map; if we use subscripts that are less than 0 or greater than MAPX-1 or MAPY-1, there will be no data to read, and the system will throw an error.  To get around this, I use these tricks to force out of bounds values into in-bounds values without changing the value of in-bounds values.

What happens if an object is out of bounds?  If the object travels right and down beyond the limits, it will seem as if the level has looped around to the left and top end, respectively.  If the object travels left and up beyond the limits, it will seem as if the level has MIRRORED and start again in reverse.

Out of bounds values are essentially garbage.  Good code should try to keep objects in bounds and deal with them quickly if they wander out (as into a bottomless pit).

IF FL(TH) AND 1 THEN CY=TRUE

If this object ignores walls, then we force the "clear" flag to read true regardless of the results of the test.

IF CY THEN @EDGE

If the way is clear, we skip down to edge detection.

IF AV<0 else="else" then="then" y="IY1*8:G=TRUE</p">SYV(TH)=2

If a collision was found, we bump the object up or down to the next map tile, depending on whether it was falling or rising.  We also make note of whether the object is grounded or it hit its head.  All vertical collisions result in resetting the Y velocity to 2 pixels/frame downward.

@EDGE
IF (FL(TH) AND 8)==0 THEN @HORIZ

If this object doesn't turn around at edges, we don't bother trying to check for them.

A=ABS(IX1%MAPX):B=ABS(IX2%MAPX):C=ABS(IY2%MAPY)
L=MAP(A,C)>0:R=MAP(B,C)>0

Taking a look at the lower-left and lower-right tiles that this object overlaps.  L is true if there is a block to the left, and R is true if there is a block to the right.

IF L AND R==FALSE THEN SXV(TH)=-ABS(SXV(TH))
IF R AND L==FALSE THEN SXV(TH)=ABS(SXV(TH))

If there is a block to the left but not to the right, then we want to stop traveling to the right; we force our X velocity to go negative.  Similarly, if there's a block to the right but not to the left, we don't want to travel to the left, so the velocity becomes positive.

@HORIZ
IF TH==0 THEN GR=G:HH=H

If we're currently examining the player object, we want to store the checks that determined whether the player was grounded or hit its head because these become important for checks we make later.

SY(TH)=Y:X=SX(TH)+SXV(TH):CX=TRUE:P=FALSE

Store the value of Y back into the object's coordinates, then update the X value for its X velocity.  CX is true if the X direction is clear.  P is true if the object is "pushing" against a wall.

IX1=FLOOR(X/8):IX2=FLOOR((X+16)/8)
IY1=FLOOR(Y/8):IY2=FLOOR((X+15)/8)
AV=SXV(TH):IF TH==0 THEN AV=SXV(0)+RXV
IF AV==0 THEN @RIDE
I=IX1:IF AV>0 THEN I=IX2
FOR J=IY1 TO IY2
A=ABS(I%MAPX):B=ABS(J%MAPY)
IF MAP(A,B)>0 THEN CX=FALSE:P=TRUE
NEXT J

These are the same sorts of checks we did before, this time looking in the X direction.

IF FL(TH) AND 1 THEN CX=TRUE

Again, if we ignore walls, we force CX to be true.

IF CX THEN @RIDE
IF FL(TH) AND 4 THEN SXV(TH)=-SXV(TH):GOTO @RIDE

If this object bounces off walls, we simply invert the X velocity when it reaches a wall and skip the rest of it.

IF AV>0 THEN X=IX1*8 ELSE X=(IX1+1)*8
SXV(TH)=0

We bump the object back to the last clear tile and stop it.

@RIDE
SX(TH)=X
IF TH==0 THEN PU=P

Update the X coordinate, and if we're dealing with the player, keep track of whether it's "pushing" a wall.

R=((FL(TH) AND 16)==16) AND SPHITSP(0,TH) AND (SY(TH)-SY(0)>8) AND (SYV(0)>=0)
IF R AND (GR==FALSE OR SYV(TH)<0 and="and" hh="=FALSE" ri="TH</p" then="then">
Here, we're checking to see if the player object is riding this object.  There are a colossal SIX conditions to check, and all of them must be met:

1) Is this object actually a platform?  If it isn't, then naturally it's not going to carry the player.

2) Is this object touching the player?

3) Is the player 8 pixels higher than the object or more?  I've played around with it, and this seems to be the threshold that allows a moving platform to "catch" the player without detecting some obvious misses.

4) Is the player falling?  We don't want to catch the player on the way up from a jump.

5) Either of these conditions must be met: the player is not already standing on the ground, or the platform is moving up.  This allows us to make platforms that can catch the player by moving up from the ground to meet it, but not by moving down into the ground.

6) The player is not hitting its head.  If a platform carries the player up into a block and the player hits its head on it, the player should stop riding this platform and drop through.

When all of these conditions are met, we will consider the player to be riding this moving platform.  If more than one object qualifies as the player's ride, we consider the one with the highest object number.

@PHYDONE
NEXT TH

And we're done!  Back to the top, check the next object.

IF SY(1)>200 THEN SYV(1)=-1
IF SY(1)<16 p="p" syv="syv" then="then">
Most object AI should be performed inside the TH loop.  This will allow you to better generalize object behavior.  If this were a real game, I would probably use the special variables associated with each sprite to keep track of what kind of thing each sprite is and use this information to dictate its behavior.  Moreover, for specialized objects (like this moving platform), I would use the sprite variables to determine where it's allowed to move.

But since this example is so simple, I put the AI for the moving platform here.  It simply checks the object's Y position and changes the direction it moves when it reaches certain boundaries.

RXV=0:RYV=0
IF RI==0 THEN @SKIPRIDE

Now we're going to deal with the player riding moving platforms.  We start by clearing out the player's "riding" velocities.  If we didn't find a platform for the player to ride on, we skip the next steps.

SY(0)=SY(RI)-15:SX(0)=SX(0)+SXV(RI):GR=TRUE

First, we position the player to make it look like it's standing neatly on top of whatever platform the player is riding on.  Then, we move the player according to the horizontal velocity of that platform.  Finally, we treat the player as if it is "grounded", so that it can jump and so on.

RXV=SXV(RI):RYV=SYV(RI)
@SKIPRIDE

We take note of the X and Y velocities of the platform the player is riding (for use in the collision tests above), and we're done.

GOSUB @SCROLL

Scrolling the screen is rather complicated.  It's moved down to a subroutine below to keep the main loop relatively clear.

BU=BUTTON():BT=BTRIG()

Store the current state of the buttons.

IF BU AND 8 THEN SXV(0)=SXV(0)+.2
IF BU AND 4 THEN SXV(0)=SXV(0)-.2

If the user is holding left or right, we increase the X velocity in the appropriate direction.

IF (BT AND 32)==32 AND GR THEN SYV(0)=-2.25:BEEP 8,-4096

I prefer the Super Mario World controls (Y=Run, B=Jump) on a DS, so here, we check if the B button has been pressed and the player is standing on stable ground.  Using the BTRIG() value instead of the BUTTON() value means that we'll only detect the press once; the player can't hold down the button to jump repeatedly.

To actually make the player jump, we simply give it an upward Y velocity and use BEEP to make the jumping noise.

IF (BU AND 32)==0 AND SYV(0)<0 p="p" syv="syv" then="then">
In Super Mario Brothers, the height of Mario's jump depends on how long you hold down the jump button.  Here's how to make that work.  If the B button isn't being held down and the player is rising, we simply kill the player's upward velocity, exactly as if it had reached the height of its jump and started falling again.

The next three lines allow you to add a "wall jump" to the player's move set.  Not every game is going to want wall jumps, so the lines are commented out to indicate that they're optional.  If you want wall jumps in your game, remove the apostrophes at the beginning of each line.  If you don't, you can remove the three lines altogether to save space.

'WJ=(GR==FALSE) AND (PU==TRUE) AND ((BT AND 32)==32)

A wall jump is valid if all three conditions are true: the player is not standing on solid ground, the player is pushing up against a wall, and the user has pressed the jump button.

'IF (BU AND 4)==4 AND WJ THEN SXV(0)=2:SYV(0)=-2.25:BEEP 8,-4096
'IF (BU AND 8)==8 AND WJ THEN SXV(0)=-2:SYV(0)=-2.25:BEEP 8,-4096

If a wall jump is valid, we do the jump.  The Y velocity and BEEP are the same as before, but we also add an X velocity according to which direction the user is pushing to kick the player away from the wall.

L=1 IF SXV(0)<0 l="-1</p" then="then">IF BU AND 128 THEN L=L*2

Calculating the maximum X velocity of the player.  If the player is moving left, the velocity will be negative, otherwise it will be positive.  If the user is holding down the Y button, we'll allow the maximum to be doubled.

IF ABS(SXV(0))>ABS(L) THEN SXV(0)=L

If we've broken the limit, we go back down to the limit.

IF SXV(0)>0 THEN SXV(0)=SXV(0)-.1
IF SXV(0)<0 p="p" sxv="sxv" then="then">
Friction.  If the player is moving right, we pull him back to the left.  If the player is moving left, we pull him back to the right.

IF ABS(SXV(0))<.1 THEN SXV(0)=0

Decimal math is very imprecise in this system, which sometimes leads to very small fractions of velocity.  Here, we just detect those small fractions we don't want and return them to zero.

VSYNC 1
GOTO @MAIN

VSYNC 1 tells the system to wait until it's time to draw the next frame.  Then we return to the top of the main loop and start again.

Now we're going to look at how the screen scrolls.  It helps if you know, before you begin, how the background is represented.  Here's my explanation of the background, copied and pasted from the comments in the last version.  Big thanks to GameFAQs forum user UncleSporky for his scrolling screen demonstration, which helped me to learn how this all works.

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

@SCROLL
MX=FLOOR(XOFS/8):MY=FLOOR(YOFS/8)

Start by figuring out which tile of the background the camera is pointing at.  We'll use this to determine if the camera has gone past a tile boundary.

MAXX=(MAPX-32)*8
MAXY=(MAPY-24)*8

Calculating the maximum X and Y coordinates for the camera.

X=SX(0)-XOFS:Y=SY(0)-YOFS

Here, we figure out where on the screen the player is.  We don't need the screen to be completely centered on the player at all times, so I've determined some "scroll zones"; if the player moves beyond a certain place in the screen, the screen will try to scroll to show what lies ahead.

IF X<=136 OR XOFS>=MAXX THEN @S2

We skip the scrolling if the player hasn't entered the right scroll area, or if the camera has already reached its limit.

DX=FLOOR(X-136):XOFS=XOFS+DX

We figure out how far into the scroll zone the player has moved, and bump the camera to the right that many pixels.

IF XOFS>MAXX THEN XOFS=MAXX

If we moved the camera too far, we bump it back into place.

IF FLOOR(XOFS/8)<=MX THEN @S2

If we haven't crossed a tile boundary, we skip the next step.

FOR I=0 TO 24
BGPUT 1,MX+33,MY+I,MAP((MX+33)%MAPX,(MY+I)%MAPY),8,0,0
NEXT I

Drawing the next column of tiles as we scroll into them.  Again, note the use of modulo math to keep our values within the proper range.  The background beyond the right and bottom edges of the map will essentially be filled with garbage data, but the camera will never scroll far enough to show it.

@S2
IF X>=120 OR XOFS<=0 THEN @S3
DX=FLOOR(120-X):XOFS=XOFS-DX
IF XOFS<0 then="then" xofs="0</p">IF FLOOR(XOFS/8)>=MX THEN @S3
FOR I=0 TO 24
BGPUT 1,MX-1,MY+I,MAP(MX-1,(MY+1)%MAPY),8,0,0
NEXT I

The same ideas as above, but scrolling left.

@S3
MX=FLOOR(XOFS/8)
IF Y<=164 OR YOFS>=MAXY THEN @S4
DY=FLOOR(Y-164):YOFS=YOFS+DY
IF YOFS>MAXY THEN YOFS=MAXY
IF FLOOR(YOFS/8)<=MY THEN @S4
FOR I=0 TO 32
BGPUT 1,MX+I,MY+25,MAP((MX+I)%MAPX,(MY+25)%MAPY),8,0,0
NEXT I

Scrolling down.

@S4
IF Y>=66 OR YOFS<=0 THEN @S5
DY=FLOOR(66-Y):YOFS=YOFS-DY
IF YOFS<0 then="then" yofs="0</p">IF FLOOR(YOFS/8)>=MY THEN @S5
FOR I=0 TO 32
BGPUT 1,MX+I,MY-1,MAP((MX+I)%MAPX,MY-1),8,0,0
NEXT I

Scrolling up.

@S5
BGOFS 1,XOFS,YOFS
RETURN

Finally, we move the camera into the correct position, and return.

Labels:


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.

ACLS:CLEAR

Just clearing the screen and memory so everything's nice and tidy.

PX=0:PY=0:PYV=2:PXV=0

(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.

XOFS=0:YOFS=0

These are the coordinates of the camera.

DIM MAP(128,128)

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.

SPSET 0,64,0,0,0,0
SPHOME 0,0,15

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
MAP(I,J)=44
NEXT J
NEXT I
FOR I=0 TO 60
MAP(I,23)=44
NEXT I
FOR I=25 TO 31
MAP(I,22)=44
NEXT I
FOR I=5 TO 18
MAP(O,I)=44
NEXT I
FOR I=0 TO 127
MAP(I,127)=44
NEXT I
MAP(20,20)=44

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
BGPUT 1,I,J,MAP(I,J),8,0,0
NEXT J
NEXT I

Before we begin the main loop, we fill the screen with data from the MAP array.

@MAIN
SPOFS 0,PX,PY

Display the player character.

X=PX+XOFS:Y=PY+PYV+YOFS:PYV=PYV+.1

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.

CY=TRUE:GR=FALSE

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=FLOOR(X/8):IX2=FLOOR((X+15)/8)
IY1=FLOOR(Y/8):IY2=FLOOR((Y-15)/8)

(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
BGREAD(1,I,J),B,C,D,E
IF B>0 THEN CY=FALSE
NEXT I

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.

@HORIZ
PY=Y-YOFS:X=PX+PXV+XOFS:CX=TRUE

Now that we're sure of the player's Y position, we update it and get ready to check for horizontal collisions.

IX1=FLOOR(X/8):IX2=FLOOR((X+15)/8)
IY1=FLOOR(Y/8):IY2=FLOOR((Y-15)/8)
I=IX1:IF PXV0 THEN I=IX2
FOR J=IY2 TO IY1
BGREAD(1,I,J),B,C,D,E
IF B>0 THEN CX=FALSE
NEXT J
IF CX==FALSE THEN X=IX*8:PXV=0
PX=X-OFS

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.

GOSUB @SCROLL

At this point, we check to see if the screen scrolls.  I decided to make this its own subroutine.

BU=BUTTON():BT=BTRIG()

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.

VSYNC 1
GOTO @MAIN

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

@SCROLL
MX=FLOOR(XOFS/8):MY=FLOOR(YOFS/8)

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.

DX=FLOOR(PX-136):XOFS=XOFS+DX:PX=PX-DX

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
BGPUT 1,MX+33,MY+I,MAP((MX+33)%128,(MY+I)%128),8,0,0
NEXT I

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.

@S2
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
BGPUT 1,MX-1,MY+I,MAP(MX-1,(MY+1)%128),8,0,0
NEXT I

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.

@S3
MX=FLOOR(XOFS/8)
IF PY<=164 OR YOFS>831 THEN @S4
DY=FLOOR(PY-164):YOFS=YOFS+DY:PY=PY-DY
IF YOFS>832 THEN YOFS=832
IF FLOOR(YOFS/8)<=MY THEN @S4
FOR I=0 TO 32
BGPUT 1,MX+I,MY+25,MAP((MX+I)%128,(MY+25)%128),8,0,0
NEXT I

Same principle again, scrolling down.

@S4
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
BGPUT 1,MX+I,MY-1,MAP((MX+I)%128,MY-1),8,0,0
NEXT I

Same idea again, scrolling up.

@S5
BGOFS 1,XOFS,YOFS
RETURN

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.

Labels:


This page is powered by Blogger. Isn't yours?