2D "Space Physics" Using BlitzBasic

 

Introduction:

One thing that I've always wanted to do is put a little 2D ship up and have it fly around on a parallaxing star field and have some halfway decent physics. I've studied a number of pieces of code to accomplish this and have taken various suggestions and just plain messed with ideas in my own head.

First off I'd like to thank the following folks for helping me...I've used a little bit from each of them to figure this out and they got me as far as getting the ship moving with the Sin and Cos functions. So...thanks to:

 

Please forgive me if my math descriptions are lacking or worse. I'm not the best at math and I'm even worse at trying to describe it. Much of the Sin/Cos stuff you'll see here I picked up as tips from those folks above and/or from the TLC Trigonometry CD I've got ;)

The source included herein is written using BlitzBasic. You can certainly get the point of the code without using BlitzBasic, but I'd highly recommend you try this increadible engine out! If you're interested in seeing all this in action, please grab the latest version of BlitzBasic by going to the BlitzBasic Website. You'll also need to have DirectX 7.0 or higher.

Quick note, I'll likely use "BB" in place of writing "BlitzBasic" everywhere...so now you'll know what that means ;)

 

Using Sin/Cos to get X, Y angle data

In order to get a ship moving at a particular angle with a balanced thrust, you'll need to use the Sin and Cos functions. Simply pass them your ship's direction multiplied by 10 and you'll get a floating point value. Multiply that value to the thrust in order to move the ship properly.

Now you can either do this calculation real-time (slow) or you can setup a table when your program launches. I decided on option two for mine...here's the code:

 


  Function PrecomputeSinCosTables()
   Local iAngle# = 0

   ; run through the full rotation cycle, and use Sin and Cos
   ; at 10-degree increments
   For iAngle = 0 To iNumRotations-1
       xSinTable#(iAngle) = -Sin(iAngle*10)
       yCosTable#(iAngle) = Cos(iAngle*10)
   Next
  End Function

Top speeds per angle

This part I had to struggle through myself and it wasn't fun. The basic problem goes like this: Top speeds don't adjust for X,Y based on direction. "Huh?" Exactly how I felt. Read on...

Let's say that the top speed of a ship is 1.0, that *should* mean the following breakdown will happen when turning our ship to the right (taken at full thrust and top speed):

0-degrees(Due North): x=0, y=1.0
10-degrees: x=.1, y=.9
20-degrees: x=.2, y=.8
...
90-degress(Due East) x=1.0, y=0
etc..

Plus we need to consider that ships have different top speeds and different thrust values.

It's easy to give a top speed of, say, 1.0 and just let your X and Y values go all the way up to, but not past, that value. Unfortunately, it's also VERY innacurate. Problem with that is at a 45-degree angle the X speed will be 1.0 AND your Y speed will be 1.0, which effectively gives the ship a full speed of 2.0...1.0 full point past it's supposed top speed.

If you don't put a cap on speed the game will be unplayable, so that's not an option either. The only method I can think of is this: As the Angle causes X to increase in velocity, Y should likewise decrease in it's velocity, thus keeping both X and Y in a constant state of change and allowance for individual top speeds.

With that in mind, I put together another set of tables that determine the top-speeds allowable for both X and Y from 0 to 80 degrees. I then just reversed these values for the 90-170, 180-260, and 270-350 degrees. Here's the code:

 


  Function PrecomputeTopSpeedTables()
   Local iSpeedControl#
   Local iXAngle = 9
   Local iYAngle = 0

   ; setup our speed modifier
   iSpeedModifier# = (Player\dTopSpeed# / (iNumRotations/4))
   ; save our Top Speed
   iSpeedControl# = Player\dTopSpeed#

   For iAngle = 0 To iNumRotations / 4
       ; assign the speed control per x,y angle
       xTSTable#(iXAngle) = iSpeedControl#
       yTSTable#(iYAngle) = iSpeedControl#

       ; make sure that arrays are being loaded 
       ; in opposite directions
       iXAngle = iXAngle - 1
       iYAngle = iYAngle + 1

       ; get that ABS value of our current speed control
       ; minus the speed modifier
       iSpeedControl# = Abs(iSpeedControl# - iSpeedModifier#)

       ; if it's below a certain level, just set it to zero
       If iSpeedControl# < .0001 Then
          iSpeedControl# = .000000
       EndIf
   Next

   ; Now we're going to load up the angle top speed tables
   ; so we can use the iShipDir to get the appropriate
   ; speed value per x,y angle when needed without having
   ; to do wacky math real-time
   For iAngle = 0 To 8
       xTopSpeedTable#(iAngle) = xTSTable#(iAngle)
       yTopSpeedTable#(iAngle) = yTSTable#(iAngle)
   Next

   iNewAngle = 9
   For iAngle = 9 To 17
       xTopSpeedTable#(iAngle) = xTSTable#(iNewAngle)
       yTopSpeedTable#(iAngle) = yTSTable#(iNewAngle)
       iNewAngle = iNewAngle - 1
   Next

   iNewAngle = 0
   For iAngle = 18 To 26
       xTopSpeedTable#(iAngle) = xTSTable#(iNewAngle)
       yTopSpeedTable#(iAngle) = yTSTable#(iNewAngle)
       iNewAngle = iNewAngle + 1
   Next

   iNewAngle = 9
   For iAngle = 27 To 35
       xTopSpeedTable#(iAngle) = xTSTable#(iNewAngle)
       yTopSpeedTable#(iAngle) = yTSTable#(iNewAngle)
       iNewAngle = iNewAngle - 1
   Next
  End Function

I know that's not exactly pretty, but it works really swell and it builds the tables in such a way that I don't have to do unnecessary math during run-time.

 

Ship Direction and Applying Thrust/Braking:

The next piece of this is how we use these tables to set the proper angle velocities. First we'll need to know the angle of the ship at all times. Every time the player his the left or right arrow key, the ship spins appropriately. Whenever the up arrow is pressed, the direction of the ship is used to grab the Sin/Cos values and apply thrust to that angle. Here's the Code for handling the direction control:

 


  ; START the RightArrow check
  If KeyDown(RightArrow) Or KeyDown(RightKPArrow) Then
     ; spin the ship to the right
     Player\iShipDir = Player\iShipDir + 1
     If(Player\iShipDir > iNumRotations-1) Then 
        Player\iShipDir = 0
     EndIf
  EndIf
  ; END the RightArrow check

  ; START the LeftArrow check
  If KeyDown(LeftArrow) Or KeyDown(LeftKPArrow) Then
     ; spin the ship to the left
     Player\iShipDir = Player\iShipDir - 1
     If(Player\iShipDir < 0) Then 
        Player\iShipDir = iNumRotations-1
     EndIf
  EndIf
  ; END the LeftArrow check

Now that we have that information, we'll need to apply the thrust. This next section of code does just that. But study this section carefully as it also handles the fluctuating speeds that I described above.

Before showing the code, I need to explain one thing. In order to get a reasonable descent during strong moves away from an X or Y angle, I opted to cheat a little. I'm using the center value as a speed modifier when past the center point on an angle. So, if the ship is pointing at 10 degrees (N-NW), the Y top speed will be very high but X will be very low. This basically means that if you were floating along for a while and had a high X top speed and low Y speed, but you wanted to turn, your X speed would take forever to get down low enough while your Y speed would get to it's new destination very quickly. This is because X will have a low modifier. Since the Center point between N and E is a high modifier for both X and Y, I use this as the default for lowering high X and Y speeds appropriately.

With any luck, that made sense. If not, hopefully the code will:

 


  ; START the UpArrow check
  ; This section controls the Thrust of the ship and so on
  ; study this one carefully
  If KeyDown(UpArrow) Or KeyDown(UpKPArrow) Then

     ; add the thrust information to the current x,y vars
     ; of the ship's directional vectors
     Player\dX# = Player\dX# + (xSinTable#(Player\iShipDir) * Player\dThrust#)
     Player\dY# = Player\dY# + (yCosTable#(Player\iShipDir) * Player\dThrust#)

     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     ;;;; This handles the N, N/E quadrant
     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     If Player\iShipDir < 9 Then

        ;Handling the X axis there
        If Player\dX# < 0 - xTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the X axis due to angle
           If Player\iShipDir < 5
              ; use the 5th xSinTable spot to decrease X speed at a reasonable rate
              Player\dX# = Player\dX# - (xSinTable#(5) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Sin information for normal decrease
              Player\dX# = Player\dX# - (xSinTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf

        ;Handling the Y axis there
        If Player\dY# > yTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the Y axis due to angle
           If Player\iShipDir > 4
              ; use the 5th xSinTable spot to decrease Y speed at a reasonable rate
              Player\dY# = Player\dY# - (yCosTable#(5) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Cos information for normal decrease
              Player\dY# = Player\dY# - (yCosTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf
     EndIf

     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     ;;;; This handles the E, S/E quadrant
     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     If Player\iShipDir >= 9 And Player\iShipDir < 18 Then

        ;Handling the X axis there
        If Player\dX# < 0 - xTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the X axis due to angle
           If Player\iShipDir > 13
              ; use the 14th xSinTable spot to decrease X speed at a reasonable rate
              Player\dX# = Player\dX# - (xSinTable#(14) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Sin information for normal decrease
              Player\dX# = Player\dX# - (xSinTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf

        ;Handling the Y axis there
        If Player\dY# < 0 - yTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the Y axis due to angle
           If Player\iShipDir < 14
              ; use the 14th xSinTable spot to decrease Y speed at a reasonable rate
              Player\dY# = Player\dY# - (yCosTable#(14) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Cos information for normal decrease
              Player\dY# = Player\dY# - (yCosTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf
     EndIf

     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     ;;;; This handles the S, S/W quadrant
     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     If Player\iShipDir >= 18 And Player\iShipDir < 27 Then

        ;Handling the X axis there
        If Player\dX# > xTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the X axis due to angle
           If Player\iShipDir < 23
              ; use the 23rd xSinTable spot to decrease X speed at a reasonable rate
              Player\dX# = Player\dX# - (xSinTable#(23) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Sin information for normal decrease
              Player\dX# = Player\dX# - (xSinTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf

        ;Handling the Y axis there
        If Player\dY# < 0 - yTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the Y axis due to angle
           If Player\iShipDir > 22
              ; use the 23rd xSinTable spot to decrease Y speed at a reasonable rate
              Player\dY# = Player\dY# - (yCosTable#(23) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Cos information for normal decrease
              Player\dY# = Player\dY# - (yCosTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf
     EndIf

     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     ;;;; This handles the W, N/W quadrant
     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     If Player\iShipDir >= 27 Then
 
        ;Handling the X axis there
        If Player\dX# > xTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the X axis due to angle
           If Player\iShipDir > 31
              ; use the 32nd xSinTable spot to decrease X speed at a reasonable rate
              Player\dX# = Player\dX# - (xSinTable#(32) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Sin information for normal decrease
              Player\dX# = Player\dX# - (xSinTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf
 
        ;Handling the Y axis there
        If Player\dY# > yTopSpeedTable#(Player\iShipDir) Then
           ; see if we need to subtract faster off the Y axis due to angle
           If Player\iShipDir < 32
              ; use the 32nd xSinTable spot to decrease Y speed at a reasonable rate
              Player\dY# = Player\dY# - (yCosTable#(32) * Player\dThrust#)
           Else
              ; Otherwise just use the normal Cos information for normal decrease
              Player\dY# = Player\dY# - (yCosTable#(Player\iShipDir) * Player\dThrust#)
           EndIf
        EndIf
     EndIf
  EndIf
  ; END the UpArrow check

Lastly, the down arrow allows the player to slow the ship down to a complete stop. To do this we simply multiply the current X, Y velocities by the Braking value, as follows:

 


  ; START the DownArrow check
  If KeyDown(DownArrow) Or KeyDown(DownKPArrow) Then
     ; slow down the ship
     Player\dX = Player\dX * Player\dBraking
     Player\dY = Player\dY * Player\dBraking

     ; check for stop on ship's X axis
     If Player\dX < .001 And Player\dX > -.001 Then
	Player\dX = 0
     EndIf
	
     ; check for stop on ship's Y axis
     If Player\dY < .001 And Player\dY > -.001 Then
	Player\dY = 0
     EndIf
   EndIf
   ; END the DownArrow check

Loading Images, Parallaxing Starfields, and Displaying the Ship:

The last piece of this equation is the movement of the starfields and the displaying of your ship at it's appropriate angle.

Firstly, though, you're going to need to load the graphics in. The following function loads in three starfield images, each being a little brighter than the last. Then it loads in a ship graphic which has only one frame. This frame is then rotated 360-degrees in 10-degree increments, placing each rotated frame in an array of images. Here's the code:

 


  Function LoadGraphics()
   Local Image_Temp   ; pointer to a Temp image

   ; be lazy and let BB handle the centering of the images
   ; during rotation
   AutoMidHandle True

   ; load up the star backgrounds (3 total)
   Image_StarsFar=LoadImage( "graphics\starsfar.bmp" )
   Image_StarsMid=LoadImage( "graphics\starsmid.bmp" )
   Image_StarsClose=LoadImage( "graphics\stars.bmp" )

   ; load up our ship image
   Image_Temp=LoadImage( "graphics\ship1.bmp" )

   ; set it's mask (transparent color)
   MaskImage Image_Temp,0,0,0

   ; now run through the loop and rotate the image
   ; around at 10 degree increments.  
   For iLoop=0 To iNumRotations-1
       Ship_Player(iLoop)=CopyImage( Image_Temp )
       RotateImage Ship_Player(iLoop),iLoop*360/iNumRotations
   Next

  End Function

When doing this type of game, it's not really the ship that you want to move around, but rather the background. This will make it appear that the ship is moving when in reality it's sitting in the center of the screen doing nothing but spinning around. The stars give it the illusion of movement.

But one layer of stars gives very little life to the environment. So we're going to use some REALLY cool functions that come with BB to help us out a bit. These functions basically tile an image on the background for us, but we can alter their default starting points...and since they'll wrap for us, we don't need to do anything more!

Now, the starfield parallaxing effect needs more explaination. Hopefully this is clear. Also, I should note that I pretty much lifted this method from the Insectoids demo.

If you load all three star maps in a paint program, you'll notice that they are identical as far as star placement goes. The difference then is that starsfar.bmp is dimmer than starsmid.bmp, and starsmid.bmp is likewise dimmer than stars.bmp. This is to give the illusion of depth. I space them out during the rendering phase so they don't overlap initially...it looks weird when they overlap.

The black parts are transparent. That's the default mask color, anyway. You could change that mask to make any color transparent using the MaskImage function. Transparency is ONLY used with TileImage. TileBlock does not use transparency (for speed reasons). This is why starsfar.bmp uses TileBlock...since everything is drawn on top of it, there's no need for transparency and so we save on speed by using TileBlock.

So, what happens is:

 

Here's the source for a 3 layer parallaxing star field (using our X, Y angle information, of course):

 


  Function RenderStarfield()
   ; Fill the whole background with the 3 star layers
   ; in their current positions
   TileBlock Image_StarsFar,Scroll_StarsX,Scroll_StarsY
   TileImage Image_StarsMid,Scroll_StarsX2*2,Scroll_StarsY2*2
   TileImage Image_StarsClose,Scroll_StarsX3*3,Scroll_StarsY3*3
	
   ; setup the 3 layers of data for the next rendering
   Scroll_StarsX = Scroll_StarsX + Player\dX
   Scroll_StarsY = Scroll_StarsY + Player\dY
   Scroll_StarsX2 = Scroll_StarsX + 7 + Player\dX
   Scroll_StarsY2 = Scroll_StarsY + 7 + Player\dY
   Scroll_StarsX3 = Scroll_StarsX + 23 + Player\dX
   Scroll_StarsY3 = Scroll_StarsY + 23 + Player\dY
  End Function

Okay, now rendering our ship is painfully simple.  So simple, it needs no explaination.
Here is the function:

  Function RenderPlayer()
   ; display the ship's current frame in the center of the screen
   DrawImage Ship_Player(Player\iShipDir),Player\iX,Player\iY
  End Function

Actually, you don't even need a function for that. I just chose to put the DrawImage call in a function to keep my steps seperated for ease of reading.

Conclusion:

I hope this is clear enough to help you understand this. This was a major brain-bender for me, so don't feel bad if it takes you awhile to get it. Also, this is NOT perfect but it's certainly good enough to get a neat little 2D space game going ;)

In order to get the full effect of this tutorial, you'll need to download and study the code (plus you can see it in action this way!). In order to read the full code, you'll need to have at least the demo version of BlitzBasic. You can get that by going to the BlitzBasic Website (it's less than 2 megs last I checked). You'll also need to have DirectX 7.0 or higher.

You can download the full source and and such for this article by CLICKING HERE!

Thanks to the creator of SpriteLib, Ari Feldman, for the little ship graphic and to BB for the starfields!

Keep an eye on this area as I plan to add more and more stuff as time permits. if you have questions, comments, or suggestions.

Until next time...cya!

This tutorial is by Christian Coders>