Lesson 17: Movement and Rotation

In a few of the previous lessons we've encountered code that moves objects around on the screen. It's time to explore the various methods that motion can be achieved using vectors, degrees, radians, and rotation.

At some point you're going to need trigonometry when making games. I know, I know, that word sounds scary and it's often at this point where budding game programmers give up. Searching the Internet for information on how to add motion and physics to games can be mind numbing. Many times the solutions provided by individuals is cryptic and down-right confusing because they assume you already have a grasp of higher math functions. That was my issue when I started writing games. The answers were there but presented in such a way that it confused me. You're not alone. The highest math I took in high school was Algebra II and by the time I was writing games those lessons were a faded memory. In this lesson I hope to break these concepts down to something everyone can understand. The math involved is surprisingly easy once you get your head wrapped around it.

Don't let the math turn you away. If you are having trouble understanding the concepts presented in this lesson head on over to the QB64 Forum and ask for help. There are quite a few forum members that have impressive math skills and I have called upon their expertise many times. There are no stupid questions!

Vectors

All objects on a computer screen use coordinates to identify where they are located. The CIRCLE statement requires a single set of coordinates to identify its center location on the screen. The LINE statement requires two sets of coordinates to identify the start and end locations of the line segment on the screen. Yet another command we will investigate shortly is _MAPTRIANGLE that requires three coordinate pairs. By modifying an object's coordinates you can effectively move an object around the screen. The direction in which an object moves is known as a vector.

A vector is a directed quantity that contains a length and direction but not position. For example lets place a circle at coordinate 200,100 on the screen. In this case the circle's X coordinate is 200 and its Y coordinate is 100.  If you want to move the circle up and to the right five pixel coordinates you would need to add 5 to the X coordinate and subtract 5 from the Y coordinate. This moves the circle to the right (X = X + 5) and up (Y = Y - 5). If placed into a loop the circle will continue to move in this direction or vector:

x = 200 '              circle's x coordinate
y = 100 '              circle's y coordinate
DO
   CIRCLE (x, y), 30 ' draw circle
    x = x + 5 '        move circle to the right 5 pixels
    y = y - 5 '        move circle up 5 pixels
LOOP

This code snippet set up two vectors for the circle. The circle has an x vector of 5 and a y vector of -5. It doesn't matter where the circle's coordinates start since all that matters to a vector is how long it is (the quantity) and the direction it travels in (the quantity's sign). The circle could be placed at 300,300 but these vector quantities will always result in the circle moving up and to the right. The vector quantities can be placed in their own variables like so:

x = 300 '              circle's x coordinate
y = 300 '              circle's y coordinate
Vx = 5 '               x vector of 5  (right)
Vy = -5 '              y vector of -5 (up)
DO
   CIRCLE (x, y), 30 ' draw circle
    x = x + Vx '       move circle x vector direction
    y = y + Vy '       move circle y vector direction
LOOP

By varying the values of Vx and Vy you can effectively have the circle move in any direction you wish. Here is a simple example showing this in practice.

( This code can be found at .\tutorial\Lesson17\BallVectorDemo.bas )

'* Simple vector demo - Bouncing Ball

CONST SWIDTH = 640, SHEIGHT = 480 ' screen width and height
CONST BALLRADIUS = 30 '             ball's radius

DIM BallX! '                        ball's x coordinate
DIM BallY! '                        ball's y coordinate
DIM BallVectorX! '                  ball's x vector
DIM BallVectorY! '                  ball's y vector

SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) '                              graphics screen
RANDOMIZE TIMER '                                                    seed RND number generator
BallX! = SWIDTH / 2 '                                                ball x location (center)
BallY! = SHEIGHT / 2 '                                               ball y location (center)
BallVectorX! = (INT(RND * 5) + 1) * SGN(RND - RND) '                 ball x vector (-5 to 5)
BallVectorY! = (INT(RND * 5) + 1) * SGN(RND - RND) '                 ball y vector (-5 to 5)
DO '                                                                 begin main loop
   _LIMIT 60 '                                                       60 frames per second
   CLS '                                                             clear screen
    BallX! = BallX! + BallVectorX! '                                 add vector to ball's x coordinate
    BallY! = BallY! + BallVectorY! '                                 add vector to ball's y coordinate
   IF BallX! >= SWIDTH - BALLRADIUS OR BallX! <= BALLRADIUS THEN '   did ball hit right/left side?
        BallVectorX! = -BallVectorX! '                               yes, reverse x vector
   END IF
   IF BallY! >= SHEIGHT - BALLRADIUS OR BallY! <= BALLRADIUS THEN '  did ball hit top/bottom side?
        BallVectorY! = -BallVectorY! '                               yes, reverse y vector
   END IF
   CIRCLE (BallX!, BallY!), BALLRADIUS '                             draw ball
   _DISPLAY '                                                        update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                            leave when ESC pressed
SYSTEM '                                                             return to operating system

Lines 15 and 16 of the code set up random vector quantities of -5 to 5 for both the X and Y vectors. Lines 20 and 21 update the circle's position by adding these vector quantities to the circle's current position. If the circle hits the side of the screen as determined by lines 22 and 25 the sign of the vector is changed. This effectively reverses the circle's direction by altering the vector by 180 degrees.

In the example code above the speed of the circle is a fixed quantity. In order to change the speed of the circle the length of the vectors (the quantity) needs to be changed. This is typically done by multiplying the vector quantity by a given speed. However the code above produces vector quantities in the range of -5 to 5 and multiplying by a fixed speed value will produce an inconsistent result. The best way to handle this is to have vectors stay within the range of -1 to 1 producing a speed that is consistent. This is done by normalizing the the vectors into unit vectors with values between -1 and 1.

To normalize a vector you simply set up a right triangle using the vector quantities. Then use the Pythagorean Theorem to calculate the length of the vector (the hypotenuse) and finally divide the original vector quantities by the calculated length. Figure 1 below demonstrates this:

Figure 1: Normalizing vector quantities

The Vx and Vy vector quantities are calculated by subtracting the x values and y values to obtain the vector lengths. You may be wondering why bother since we already know the vector quantities. We'll get to that a little later in the lesson. Vx and Vy are now side A and side B of the right triangle. The length of the vector is calculated using the Pythagorean Theorem. Finally, that length is used to divide the original Vx and Vy vector quantities producing a result of -1 to 1. Now a speed multiplier can properly be applied to the vector quantities as the following example program illustrates.

( This code can be found at .\tutorial\Lesson17\BallSpeedDemo.bas )

'* Simple vector demo - Bouncing Ball

CONST SWIDTH = 640, SHEIGHT = 480 '    screen width and height
CONST BALLRADIUS = 30 '                ball's radius

DIM BallX! '  ball's x coordinate
DIM BallY! '  ball's y coordinate
DIM Vx! '     ball's x vector
DIM Vy! '     ball's y vector
DIM Length! ' length of vector
DIM Speed% '  speed of ball

SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) '                             graphics screen
RANDOMIZE TIMER '                                                   seed RND number generator
BallX! = SWIDTH / 2 '                                               ball x location (center)
BallY! = SHEIGHT / 2 '                                              ball y location (center)
Speed% = 5 '                                                        initial ball speed
Vx! = (INT(RND * 5) + 1) * SGN(RND - RND) '                         ball x vector (-5 to 5)
Vy! = (INT(RND * 5) + 1) * SGN(RND - RND) '                         ball y vector (-5 to 5)
Length! = SQR(Vx! * Vx! + Vy! * Vy!) '                              length of vector
Vx! = Vx! / Length! '                                               normalize vector
Vy! = Vy! / Length!

DO '                                                                begin main loop
   _LIMIT 60 '                                                      60 frames per second
   CLS '                                                            clear screen
   PRINT '                                                          instructions
   PRINT "       Press right arrow key to increase speed, left arrow to decrease."
   LOCATE 3, 35: PRINT "Speed ="; Speed% '                          speed indicator
   IF _KEYDOWN(19712) THEN Speed% = Speed% + 1 '                    increase speed when right arrow pressed
   IF _KEYDOWN(19200) THEN Speed% = Speed% - 1 '                    decrease speed when left arrow pressed
   IF Speed% < 1 THEN Speed% = 1 '                                  obey the speed limit
   IF Speed% > 20 THEN Speed% = 20
    BallX! = BallX! + Vx! * Speed% '                                add vector to ball's x coordinate
    BallY! = BallY! + Vy! * Speed% '                                add vector to ball's y coordinate
   IF BallX! >= SWIDTH - BALLRADIUS OR BallX! <= BALLRADIUS THEN '  did ball hit right/left side?
        Vx! = -Vx! '                                                yes, reverse x vector
   END IF
   IF BallY! >= SHEIGHT - BALLRADIUS OR BallY! <= BALLRADIUS THEN ' did ball hit top/bottom side?
        Vy! = -Vy! '                                                yes, reverse y vector
   END IF
   CIRCLE (BallX!, BallY!), BALLRADIUS '                            draw ball
   _DISPLAY '                                                       update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                           leave when ESC pressed
SYSTEM '                                                            return to operating system

In lines 18 through 22 we used the formula in Figure 1 above to normalize the vectors to between -1 and 1. In lines 34 and 35 the vector quantities are added to the circle's location just like before but this time the value in speed% is multiplied to the vector quantity effectively increasing the number of pixels the circle moves in the same direction. The value in speed% is kept between 1 and 20 guaranteeing 1 to 20 pixel movements for the circle. In the first code example this could not be guaranteed because the vectors were not normalized. Each time the code would have been run the maximum speed would have varied wildly.

The _HYPOT Statement

The SQR statement (square root) was used in the previous two code examples to calculate the length of the vector using the Pythagorean Theorem. SQR is used to calculate the hypotenuse of the triangle that the theorem works with. However, SQR is a very slow statement which is why QB64 offers the _HYPOT statement. The _HYPOT statement calculates the hypotenuse given the other two sides of the triangle:

Length! = _HYPOT(SideA!, SideB!) ' calculates the hypotenuse

You simply supply the side lengths and don't need to worry about squaring them first. _HYPOT is faster than using SQR so for the rest of this course _HYPOT will be used in lieu of SQR for length and distance calculations. The previous two code examples only control one object on the screen and you would not be able to tell the difference between SQR and _HYPOT. However, when you start creating games that control hundreds of objects on the screen these optimizations make a huge difference.

Smart Vectors

Up to this point all of the example code has had an object rambling around with no purpose other than to bounce off the sides of the screen. In games you want enemies to chase you and their bullets to fire in your direction. You also want to be able to shoot your bullets in any given direction you desire. Using vectors is one of the simplest methods of achieving this. Remember these calculations in Figure 1 above?

Vx = x2 - x1
Vy = y2 - y1

These two statements now come into full use when you want to make a vector that points toward another object. Instead of using the coordinate (0, 0) for that start of the vectors as Figure 1 demonstrates you would use the coordinates of the object that you want to get a vector from. The end of the vectors are the coordinate of the object you are pointing to.

Remember, a vector does not care about location as it only contains length and direction. So a vector coordinate starting at one object and ending at another object's coordinates is perfectly fine, it's still a vector, a simple directed quantity. You simply make a right triangle between the two objects and solve for the equation in Figure 1 above. The result will be a normalized vector pair that can be applied to another object, such as a bullet, that flies from the first object to the second one. Or you can use the vector pair to direct the from object towards the to object as the following example code illustrates.

( This code can be found at .\tutorial\Lesson17\TagVectorDemo.bas )

'* Vector Demo - Tag, you're it!

TYPE XYPAIR '                    2D point definition
    x AS SINGLE '                x coordinate
    y AS SINGLE '                y coordinate
END TYPE

TYPE A_CIRCLE '                  circle definition
    loc AS XYPAIR '              x,y coordinate location
    vec AS XYPAIR '              x,y vectors
    r AS INTEGER '               radius
END TYPE

DIM RCir AS A_CIRCLE '           a red circle
DIM GCir AS A_CIRCLE '           a green circle
DIM Speed AS INTEGER '           speed of red circle

SCREEN _NEWIMAGE(640, 480, 32) ' graphics screen
_MOUSEHIDE '                     hide operating system mouse pointer
RCir.loc.x = 319 '               define circle properties
RCir.loc.y = 239
RCir.r = 30
GCir.r = 30
Speed = 3
DO '                                                                           begin main program loop
   _LIMIT 60 '                                                                 60 frames per second
   CLS
   WHILE _MOUSEINPUT: WEND '                                                   get latest mouse information
    GCir.loc.x = _MOUSEX '                                                     update player location
    GCir.loc.y = _MOUSEY
   CIRCLE (GCir.loc.x, GCir.loc.y), GCir.r, _RGB32(0, 255, 0) '                draw player
   PAINT (GCir.loc.x, GCir.loc.y), _RGB32(0, 127, 0), _RGB32(0, 255, 0)
    P2PVector RCir.loc, GCir.loc, RCir.vec '                                   calculate enemy vectors
   CIRCLE (RCir.loc.x, RCir.loc.y), RCir.r, _RGB32(255, 0, 0) '                draw enemy
   PAINT (RCir.loc.x, RCir.loc.y), _RGB32(127, 0, 0), _RGB32(255, 0, 0)
   LINE (RCir.loc.x, RCir.loc.y)-_
        (RCir.loc.x + RCir.vec.x * RCir.r, RCir.loc.y + RCir.vec.y * RCir.r) ' white line from center
   IF P2PDistance(RCir.loc, GCir.loc) > GCir.r + RCir.r THEN '                 are circles colliding?
        RCir.loc.x = RCir.loc.x + RCir.vec.x * Speed '                         no, update enemy location
        RCir.loc.y = RCir.loc.y + RCir.vec.y * Speed
       LOCATE 2, 39: PRINT "TAG" '                                             print title of game
   ELSE '                                                                      yes, circles colliding
       LOCATE 2, 35: PRINT "You're it!" '                                      print message to player
   END IF
   _DISPLAY '                                                                  update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                      leave when ESC pressed
SYSTEM '                                                                       return to operating system

'------------------------------------------------------------------------------------------------------------

SUB P2PVector (P1 AS XYPAIR, P2 AS XYPAIR, V AS XYPAIR)

    '** NOTE: V passed by reference is altered

    '** Point to Point Vector Calculator
    '** Returns x,y vectors from P1 to P2

    ' P1.x, P1.y = FROM coordinate            (INPUT )
    ' P2.x, P2.y = TO coordinate              (INPUT )
    ' V.x, V.y   = normalized vectors to P2   (OUTPUT)

   DIM D AS SINGLE ' distance between points

    V.x = P2.x - P1.x '     horizontal distance (  side A  )
    V.y = P2.y - P1.y '     vertical distance   (  side B  )
    D = _HYPOT(V.x, V.y) '  direct distance     (hypotenuse)
   IF D = 0 THEN EXIT SUB ' can't divide by 0
    V.x = V.x / D '         normalize x vector (  -1 to 1 )
    V.y = V.y / D '         normalize y vector (  -1 to 1 )

END SUB

'------------------------------------------------------------------------------------------------------------

FUNCTION P2PDistance (P1 AS XYPAIR, P2 AS XYPAIR)

    '** Point to Point Distance Calculator
    '** Returns the distance between P1 and P2

    ' P1.x, P1.y - FROM coordinate (INPUT)
    ' P2.x, P2.y - TO   coordinate (INPUT)
    ' returns SQR((P2.x - P1.x)^2 + (P2.y - P1.y)^2) using QB64 _HYPOT() function

    P2PDistance = _HYPOT(P2.x - P1.x, P2.y - P1.y) ' return direct distance (hypotenuse)

END FUNCTION

'------------------------------------------------------------------------------------------------------------

Figure 2: Tag, You're it!

By calculating the vector between the two circles and applying that vector to the red circle it becomes "smart" and can directly follow the green circle that you control. Before we get into the code a few things to note:

Lines 8 through 12 create a TYPE definition for a circle object containing an x,y coordinate pair, an x,y vector pair, and a radius. The x,y coordinate pair is a circle's current location and the x,y vector pair is the direction a circle is traveling in.

Lines 14 and 15 are setting up the player and enemy circle objects that can be manipulated around the screen.

The player's position is updated and drawn in lines 28 through 32.

Line 33 is where the magic happens:

P2PVector RCir.loc, GCir.loc, RCir.vec '                                   calculate enemy vectors

The red circle and green circle's positions are passed to the function where the vector normalizing formula is performed. The normalized vectors are then returned and placed into RCir.vec. P2PVector is a subroutine and unlike a function it can't return a result in its name so RCir.vec that is passed by reference is modified within the subroutine and those changes are returned. The red circle's vector values have now been altered and point directly at the green circle.

In line 38 a function called P2PDistance is passed both circle's locations and if the distance between the circles is greater than both radii added together the red circle's location is updated in lines 39 and 40. Line 38 is a basic circle collision detector. Lines 39 and 40 use the updated vector values to move the red circle in the direction of the green circle.

Loop back around and do the whole process again. No matter how often the green circle is moved around line 33 will guarantee that the normalized vectors are calculated to keep the red circle traveling toward the green circle.

The white line inside the red circle that always points towards the green circle is created in line 36 (and 37). A line is drawn from the center of the red circle to a point in the same direction as the red circle's vector value multiplied by its radius. It's the same concept as multiplying the vector by a speed, just in this case we use the circle's radius as one big leap.

Controlling Multiple Objects

Using vectors along with arrays allows you to control many directed objects on the screen at once as the following example code illustrates. Move the cross hair around the screen with the mouse and use the left mouse button to fire a bullet. Hold the left mouse button to increase the fire power and speed of the bullet.

( This code can be found at .\tutorial\Lesson17\VectorShootDemo.bas )

'* Vector demo - Turret shooter

CONST FALSE = 0, TRUE = NOT FALSE '  boolean truth detectors
CONST SWIDTH = 640, SHEIGHT = 480 '  screen dimensions
CONST YELLOW = _RGB32(255, 255, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST DARKGREEN = _RGB32(0, 127, 0)
CONST BLACK = _RGB32(0, 0, 0)
CONST GRAY = _RGB32(64, 64, 64)
CONST RED = _RGB32(255, 0, 0)
CONST MAGENTA = _RGB32(255, 0, 255)

TYPE XYPAIR '               2D point definition
    x AS SINGLE '           x coordinate
    y AS SINGLE '           y coordinate
END TYPE

TYPE BULLET '               bullet definition
    loc AS XYPAIR '         location of bullet
    vec AS XYPAIR '         vector of bullet
    speed AS SINGLE '       bullet speed
    col AS _UNSIGNED LONG ' bullet color
    active AS INTEGER '     active status of bullet
END TYPE

REDIM Bullet(0) AS BULLET ' dynamic bullet array
DIM CrossHair AS XYPAIR '   crosshair coordinates
DIM Center AS XYPAIR '      center of screen coordinates
DIM Vector AS XYPAIR '      normalized vector from gun barrel to crosshair
DIM Barrel AS XYPAIR '      barrel coordinates
DIM ButtonHeld AS INTEGER ' left mouse button flag
DIM Index AS INTEGER '      index number of free spot in bullet array
DIM Speed AS SINGLE '       bullet speed

SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) '                                   graphics screen
_MOUSEHIDE '                                                              hide operating system mouse pointer
Center.x = SWIDTH / 2 '                                                   screen center coordinates
Center.y = SHEIGHT / 2
Speed = 3 '                                                               initial bullet speed
DO '                                                                      begin main loop
   _LIMIT 60 '                                                            60 frames per second
   CLS '                                                                  clear screen
   WHILE _MOUSEINPUT: WEND '                                              get latest mouse update
    CrossHair.x = _MOUSEX '                                               set crosshair coordinates
    CrossHair.y = _MOUSEY
   LINE (CrossHair.x - 10, CrossHair.y)-(CrossHair.x + 10, CrossHair.y) ' draw crosshair
   LINE (CrossHair.x, CrossHair.y - 10)-(CrossHair.x, CrossHair.y + 10)
    P2PVector Center, CrossHair, Vector '                                 get vector from center to crosshair
    Barrel.x = Center.x + Vector.x * 18 '                                 calculate barrel location
    Barrel.y = Center.y + Vector.y * 18
   CIRCLE (Center.x, Center.y), 30, GREEN '                               draw gun turret
   PAINT (Center.x, Center.y), DARKGREEN, GREEN
   CIRCLE (Barrel.x, Barrel.y), 10, BLACK '                               draw gun barrel
   PAINT (Barrel.x, Barrel.y), GRAY, BLACK
   PRINT "   Power" '                                                     draw power meter
   LINE (10, 16)-(81, 31), YELLOW, B
   LOCATE 3, 4: PRINT USING "###%"; ((Speed - 3) / 7) * 100
   IF _MOUSEBUTTON(1) THEN '                                              left mouse button down?
        ButtonHeld = TRUE '                                               yes, remember this
       IF Speed < 10 THEN Speed = Speed + .25 '                           increase speed while button down
       LINE (11, 17)-((Speed - 2) * 10, 30), _RGB32(64 + Speed * 19, 0, 0), BF ' update power meter
   END IF
   IF NOT _MOUSEBUTTON(1) AND ButtonHeld THEN '                           was left button released?
        ButtonHeld = FALSE '                                              yes, remember this
        Index = FreeIndex '                                               get free index in bullet array
        Bullet(Index).active = TRUE '                                     mark bullet as active
        Bullet(Index).loc = Barrel '                                      location of bullet
        Bullet(Index).vec = Vector '                                      vector of bullet
        Bullet(Index).speed = Speed '                                     speed of bullet
        Bullet(Index).col = _RGB32(64 + Speed * 19, 0, 0) '               color of bullet match meter
        Speed = 3 '                                                       reset bullet speed
   END IF
    DrawBullets '                                                         draw active bullets to screen
   _DISPLAY '                                                             update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                 leave when ESC key pressed
SYSTEM '                                                                  return to operating system

'------------------------------------------------------------------------------------------------------------

SUB DrawBullets ()

    '** Draws all active bullets to the screen and maintains bullet array

   SHARED Bullet() AS BULLET ' bullet array
   DIM Count AS INTEGER '      array counter
   DIM Clean AS INTEGER '      if true then no active bullets

    Count = -1 '                                                                   reset array counter
    Clean = TRUE '                                                                 assume no active bullets
   DO '                                                                            begin bullet draw loop
        Count = Count + 1 '                                                        increment counter
       IF Bullet(Count).active THEN '                                              bullet active?
            Bullet(Count).loc.x = Bullet(Count).loc.x + Bullet(Count).vec.x * Bullet(Count).speed 'update bullet
            Bullet(Count).loc.y = Bullet(Count).loc.y + Bullet(Count).vec.y * Bullet(Count).speed
           CIRCLE (Bullet(Count).loc.x, Bullet(Count).loc.y), 10, MAGENTA '                       draw bullet
           PAINT (Bullet(Count).loc.x, Bullet(Count).loc.y), Bullet(Count).col, MAGENTA
           CIRCLE (Bullet(Count).loc.x, Bullet(Count).loc.y), 10, RED
           IF Bullet(Count).loc.x < -5 OR Bullet(Count).loc.x > SWIDTH + 5 THEN '  bullet leave left/right?
                Bullet(Count).active = FALSE '                                     yes, bullet now inactive
           END IF
           IF Bullet(Count).loc.y < -5 OR Bullet(Count).loc.y > SHEIGHT + 5 THEN ' bullet leave up/down?
                Bullet(Count).active = FALSE '                                     yes, bulllet now inactive
           END IF
            Clean = FALSE '                                                        active bullets exist
       END IF
   LOOP UNTIL Count = UBOUND(Bullet) '                                             leave when all checked
   IF Clean THEN REDIM Bullet(0) AS BULLET '                                       reset array if no bullets

END SUB

'------------------------------------------------------------------------------------------------------------

FUNCTION FreeIndex ()

    '** Finds or creates a free index within the bullet array

   SHARED Bullet() AS BULLET ' bullet array
   DIM Count AS INTEGER '      array counter
   DIM Index AS INTEGER '      free array index

    Count = -1 '                                              set array counter
   DO '                                                       begin array search loop
        Count = Count + 1 '                                   increment counter
       IF NOT Bullet(Count).active THEN Index = Count '       use this index if bullet inactive
   LOOP UNTIL (Count = UBOUND(Bullet)) OR Index '             leave when index found or full count
   IF NOT Index THEN '                                        was a free index in the array found?
       REDIM _PRESERVE Bullet(UBOUND(Bullet) + 1) AS BULLET ' no, increase array size by one
        Index = UBOUND(Bullet) '                              use new index for next bullet
   END IF
    FreeIndex = Index '                                       return free array index

END FUNCTION

'------------------------------------------------------------------------------------------------------------

SUB P2PVector (P1 AS XYPAIR, P2 AS XYPAIR, V AS XYPAIR)

    '** NOTE: V passed by reference is altered

    '** Point to Point Vector Calculator
    '** Returns x,y vectors from P1 to P2

    ' P1.x, P1.y = FROM coordinate            (INPUT )
    ' P2.x, P2.y = TO coordinate              (INPUT )
    ' V.x, V.y   = normalized vectors to P2   (OUTPUT)

   DIM D AS SINGLE ' distance between points

    V.x = P2.x - P1.x '     horizontal distance (  side A  )
    V.y = P2.y - P1.y '     vertical distance   (  side B  )
    D = _HYPOT(V.x, V.y) '  direct distance     (hypotenuse)
   IF D = 0 THEN EXIT SUB ' can't divide by 0
    V.x = V.x / D '         normalize x vector (  -1 to 1 )
    V.y = V.y / D '         normalize y vector (  -1 to 1 )

END SUB

'------------------------------------------------------------------------------------------------------------

Figure 3: The start of a bullet hell game

All of the action controlled on the screen is done with nothing more than normalized vectors. All of the variables needed to manipulate each individual bullet is held in a dynamic array named Bullet(). The dynamic array is allowed to grow as needed to accommodate more bullets and reset when no active bullets are present. Each bullet's location ( Bullet().loc ), vector ( Bullet().vec ), speed ( Bullet().speed ), color ( Bullet().col ), and status ( Bullet().active ) are all maintained in the array using the TYPE definition BULLET.

The DrawBullets subroutine is used to draw active bullets onto the screen with the information provided in the array. If the subroutine detects there are no active bullets left the Bullet() array it is reset back to a zero index. This ensures that the array only grows as large as needed for any given amount of bullets on the screen.

The FreeIndex function scans the array for any indexes where the bullets have become inactive and returns that index value to place the next bullet into. If the function detects that there are no free index values available it increases the array size by one and passes that new index value back to place the next bullet into. By reusing inactive indexes and only increasing the array size when necessary the array size is kept as small as possible at all times. If you are familiar with "Bullet Hell" games you know there can literally be hundreds of bullets flying around the screen at any given time. Using a fixed static array would be possible to track them all but the method of using an array that can adjust to the amount of objects needed is ideal.

Where are the variable type identifiers?

If you haven't noticed yet you don't need to use variable type identifiers, such as % for integer and ! for single, to identify a variable's type. The first two code examples use type identifiers on all of the variables. However the following two code examples did not. These two lines mean exactly the same thing:

DIM Count%
DIM Count AS INTEGER

As well as these:

DIM KeyPress$
DIM KeyPress AS STRING

You can even control the size of strings by doing this:

DIM KeyPress AS STRING * 1 ' a string variable with a length of one

We discussed in an earlier lesson that the variable declarations inside of TYPE definitions require that variables type identifiers be spelled out:

TYPE XYPAIR
    x AS INTEGER
    y AS SINGLE
END TYPE

However this method of identifying variable types can be used everywhere. This is a personal preference of the programmer. The main advantage of using type identifiers is that it makes it easy to know the type a variable is no matter where it resides in the source code. When code starts getting very large all of the type identifiers can make code seem cluttered (in my opinion, perhaps not another programmer's). From this point on all example code will have variable type identifiers spelled out.

Degrees and Radians

Vectors are perfect for setting objects in motion and pointing an object toward a known point but what if you want your object to travel at a precise 34.5 degree angle? Which vector quantity values would you need to use to achieve this? The answer lies with incorporating radian and degree values found around the perimeter of a circle.

A circle contains radian points on its perimeter ranging in value from 0 to 6.2831852, or 0 to 2 times the value of Pi (3.1415926). A radian is a direct relationship between the radius of the circle and its arc. If you want to read more about that relationship you can visit this page. All you need to know to incorporate this knowledge into game programming is that these values exist as shown in Figure 4 below.

Figure 4: Radians, degrees, and the associated conversion formulas

The points around a circle's perimeter can also be expressed in degrees also shown in Figure 4 above. The two formulas needed to convert between the two systems are given as well. QB64 makes the conversion process simple as it offers two statements to perform the calculations for you.

The _D2R and _R2D Statements

The _D2R statement takes a given degree value from 0 to 359.9999 and converts it to the equivalent radian:

Radian! = _D2R(Degree!) ' convert degree to radian

and the _R2D statement takes a given radian value from 0 to 6.2831852 and converts it the equivalent degree:

Degree! = _R2D(Radian!) ' convert radian to degree

Degrees and Radians to Vector

Once you have either a desired degree or radian heading you can then convert that value to a vector as shown in Figure 5 below.

Figure 5: Radian and degree to vector conversion

Radians can be converted directly to vector quantities using the SIN() (sine) and COS() (cosine) functions. Degrees will need to be converted to radians first as shown in the DEGREE TO VECTOR conversion in Figure 5. The degree to radian conversion formula seen in Figure 4 is embedded within the SIN() and COS() functions. Using _D2R the conversions would look like this:

Vx! = SIN(_D2R(Degree!)) '  calculate the x vector quantity
Vy! = -COS(_D2R(Degree!)) ' calculate the y vector quantity

This lesson isn't going to go into any discussion about sine and cosine and how they relate to radians. Again, all you need to know for game programming is that these functions are used to convert radian and degree points around the perimeter of a circle to vector quantities. If you wish to read more about sine and cosine you can visit this web site.

Vector Graphics

Ok, enough with the formulas for now. Let's put this information to practical use with an example program. Use the RIGHT and LEFT ARROW keys to rotate the space ship. Use the UP ARROW key to move the ship in the direction it is facing.

( This code can be found at .\tutorial\Lesson17\RadianShipDemo.bas )

'** Radian Ship Demo

CONST FALSE = 0, TRUE = NOT FALSE '     truth detectors
CONST PI = 3.1415926, PI2 = PI * 2 '    useful PI values
CONST ONEDEG = PI2 / 360 '              one radian degree

TYPE XYPAIR '                           x,y value definition
    x AS SINGLE '                       x value
    y AS SINGLE '                       y value
END TYPE

TYPE SHIP '                             ship point definition
    Vx AS SINGLE '                      point's x vector
    Vy AS SINGLE '                      point's y vector
    rad AS INTEGER '                    radian array index point sits at
END TYPE

DIM Ship(3) AS SHIP '                   radian and vector quantities of each ship point
DIM ShipLoc AS XYPAIR '                 ship's location on screen
DIM ShipSpeed AS INTEGER '              ship's speed
DIM ShipSize AS INTEGER '               ship's size
DIM Vector AS XYPAIR '                  vector locations for hidden/showing radian circle
DIM Rad(72) AS SINGLE '                 72 radian points 5 degrees apart
DIM Radian AS SINGLE '                  radian counter
DIM Count AS INTEGER '                  generic counter
DIM LeftArrow AS INTEGER '              left arrow key toggle flag
DIM RightArrow AS INTEGER '             right arrow key toggle flag
DIM ShowRads AS INTEGER '               show radian circle toggle flag

Count = 0 '                             store radian points in array
FOR Radian = 0 TO PI2 STEP ONEDEG * 5 ' 72 radian points 5 degrees apart
    Rad(Count) = Radian '               save radian value
    Count = Count + 1 '                 increment counter
NEXT Radian
Ship(1).rad = 0 '                       ship's nose cone  pointing toward radian 0        or Rad(0)
Ship(2).rad = 28 '                      ship's right side pointing toward radian .4886921 or Rad(28)
Ship(3).rad = 44 '                      ship's left  side pointing toward radian .7679448 or Rad(44)
ShipLoc.x = 319 '                       ship's x coordinate on screen
ShipLoc.y = 239 '                       ship's y coordinate on screen
ShipSpeed = 5 '                         ship's speed
ShipSize = 60 '                         ship's size
SCREEN _NEWIMAGE(640, 480, 32) '                                             graphics screen
DO '                                                                         begin main loop
   _LIMIT 60 '                                                               60 frames per second
   CLS '                                                                     clear screen
   LOCATE 2, 11: PRINT "Right/Left Arrow keys to turn, Up Arrow key to move forward"; ' print directions
   LOCATE 3, 25: PRINT "Spacebar to toggle radian circle"
   FOR Count = 1 TO 3 '                                                      cycle through 3 ship points
        Ship(Count).Vx = SIN(Rad(Ship(Count).rad)) '                         calculate vector of radian
        Ship(Count).Vy = -COS(Rad(Ship(Count).rad))
       IF ShowRads THEN '                                                    draw radian lines if active
           LINE (ShipLoc.x, ShipLoc.y)_
                -(ShipLoc.x + Ship(Count).Vx * ShipSize * 2, ShipLoc.y + Ship(Count).Vy * ShipSize * 2)
       END IF
       IF LeftArrow THEN '                                                   was left arrow key pressed?
            Ship(Count).rad = Ship(Count).rad - 1 '                          yes, update point index location
           IF Ship(Count).rad < 0 THEN Ship(Count).rad = 71 '                reset to last index if needed
       END IF
       IF RightArrow THEN '                                                  was left arrow key pressed?
            Ship(Count).rad = Ship(Count).rad + 1 '                          yes, update point index location
           IF Ship(Count).rad > 71 THEN Ship(Count).rad = 0 '                reset to first index if needed
       END IF
   NEXT Count
   LINE (ShipLoc.x + Ship(1).Vx * SHIPSIZE, ShipLoc.y + Ship(1).Vy * SHIPSIZE)_
        -(ShipLoc.x + Ship(2).Vx * SHIPSIZE, ShipLoc.y + Ship(2).Vy * SHIPSIZE) ' draw the ship
   LINE -(ShipLoc.x + Ship(3).Vx * ShipSize, ShipLoc.y + Ship(3).Vy * ShipSize)
   LINE -(ShipLoc.x + Ship(1).Vx * ShipSize, ShipLoc.y + Ship(1).Vy * ShipSize)
   IF ShowRads THEN '                                                        did user select to show radians?
       FOR Count = 0 TO 71 '                                                 yes, cycle through radian array
            Vector.x = SIN(Rad(Count)) '                                     calculate vector of radian
            Vector.y = -COS(Rad(Count))
           PSET (ShipLoc.x + Vector.x * ShipSize, ShipLoc.y + Vector.y * ShipSize) ' draw pixel at radian point
       NEXT Count
   END IF
   IF _KEYDOWN(18432) THEN '                                                 up arrow key pressed?
        ShipLoc.x = ShipLoc.x + Ship(1).Vx * ShipSpeed '                     yes, calculate vector of ship
        ShipLoc.y = ShipLoc.y + Ship(1).Vy * ShipSpeed
       IF ShipLoc.x < -ShipSize / 2 THEN ShipLoc.x = 640 + ShipSize / 2 '    keep ship on screen
       IF ShipLoc.x > 640 + ShipSize / 2 THEN ShipLoc.x = -ShipSize / 2
       IF ShipLoc.y < -ShipSize / 2 THEN ShipLoc.y = 480 + ShipSize / 2
       IF ShipLoc.y > 480 + ShipSize / 2 THEN ShipLoc.y = -ShipSize / 2
   END IF
   IF _KEYDOWN(19200) THEN LeftArrow = TRUE ELSE LeftArrow = FALSE '         toggle flag when left pressed
   IF _KEYDOWN(19712) THEN RightArrow = TRUE ELSE RightArrow = FALSE '       toggle flag when right pressed
   IF _KEYHIT = 32 THEN ShowRads = NOT ShowRads '                            toggle flag when space bar pressed
   _DISPLAY '                                                                update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                    leave when ESC pressed
SYSTEM '                                                                     return to operating system

Figure 5: A ship made with radians and vectors (Asteroids)

This program allows you to envision the underlying imaginary circle that controls everything. The center of the ship is the center of the circle and the ship's vertices are simply radian values on that circle. The radian that creates the ship's nose point is used to calculate a vector quantity from. Which ever direction the nose is pointing is the direction the ship will travel because the vectors are calculated using this radian.

Lines 30 through 34 fill the Rad() array with 72 radian values spaced evenly 5 degrees apart from each other. Lines 35 through 37 identify the radian points on the circle where the ships lines are to be drawn creating the outline of the ship. Rad(0) is the front of the ship, Rad(28) is the lower right corner of the ship, and Rad(44) is the lower left corner of the ship. These three array index values are held in the Ship() array.

When the ship is rotated to the right these three values are increased by one. If one of the index values should happen to pass the highest index value of 71 it is reset back to 0 which now points to the beginning of the circle. Likewise when the ship is rotated to the left the three values are decreased by one. If one of the index values should happen to pass the lowest index value of 0 it is reset back to 71 which now points to the end of the circle. The left and right motion is handled in lines 55 through 62 within the FOR ... NEXT loop.

Also within the FOR ... NEXT loop in lines 48 through 63 each of three values has a vector calculated from it. Lines 49 and 50 use the radian value stored in the Rad() array to calculate a vector. Remember, vectors don't care about location, only a length and direction and these two lines of code calculate that for us.

The location is now added to these vectors in lines 64 through 67. The length and direction of each vector is multiplied by ShipSize and then added to the ship's x and y coordinates. ShipSize is nothing more than the radius of the imaginary circle that follows the ship around. This brings the three points out to the perimeter of the circle where the LINE command is used to "connect the dots" of the three points to draw the ship.

Lines 75 through 82 of the code control the motion of the ship. When the UP ARROW key is pressed lines 76 and 77 use the vector quantity information stored in Ship(1).Vx and Ship(1).Vy to calculate a new ship location. The Ship(1) vector quantities are related to the front of the ship so the ship moves in the current direction the ship is pointing.

This method of calculating vector quantities from radian points to draw lines on the screen is often referred to as Vector Graphics and was used in early arcade games such as Asteroids and Tempest. By using radians to point in a given direction and then converting that radian value to vector quantities you can now control in which direction an object points and moves without the need to reference another point as is required for vector math alone.

The example above works perfect for objects that fit neatly into a circle such as the ship did. With a bit of modification this method can also be used for irregular shapes that do not fit neatly into a circle. The next code example shows how irregular shaped objects can be stored in an array and then used later on to create the shape in any position, size, or rotation desired.

( This code can be found at .\tutorial\Lesson17\RadianAsteroidDemo.bas )

'** Asteroid demo using radians

CONST FALSE = 0, TRUE = NOT FALSE '      truth detectors
CONST PI = 3.1415926, PI2 = 2 * PI '     useful PI values
CONST SPINRATE = PI2 / 360 '             asteroid spin rate
CONST SWIDTH = 640, SHEIGHT = 480 '      screen dimensions
CONST MAXASTEROIDS = 20 '                number of asteroids on screen

TYPE XYPAIR '                            2D point location definition
    x AS SINGLE '                        x coordinate
    y AS SINGLE '                        y coordinate
END TYPE

TYPE OBJECT '                            object definition (points that make up an object)
    Radian AS SINGLE '                   direction of point
    Radius AS SINGLE '                   distance to point from center
END TYPE

TYPE ASTEROID '                          asteroid definition
   Loc AS XYPAIR '                       asteroid location
    Dir AS SINGLE '                      asteroid radian direction
    Speed AS INTEGER '                   asteroid speed
    Size AS INTEGER '                    asteroid size
END TYPE

DIM Object(10) AS OBJECT '               object point data
DIM Asteroid(MAXASTEROIDS) AS ASTEROID ' asteroids array
DIM Vector AS XYPAIR '                   vector calculations
DIM Obj AS INTEGER '                     object counter
DIM Ast AS INTEGER '                     asteroid counter
DIM P1 AS XYPAIR '                       first object point for PSET
DIM Np AS XYPAIR '                       next object point for LINE
DIM Spin AS INTEGER '                    TRUE to activate spin, FALSE otherwise

RANDOMIZE TIMER '                                                        seed RND generator
FOR Obj = 1 TO 10 '                                                      cycle through object points
   READ Vector.x, Vector.y '                                             get object x,y vector point
    Object(Obj).Radius = _HYPOT(Vector.x, Vector.y) '                    calculate radius from vector
    Object(Obj).Radian = _ATAN2(Vector.x, Vector.y) '                    calculate direction from vector
NEXT Obj
FOR Ast = 1 TO MAXASTEROIDS '                                            cycle through asteroids
    Asteroid(Ast).Loc.x = INT(RND * (SWIDTH - 40)) + 20 '                random location
    Asteroid(Ast).Loc.y = INT(RND * (SHEIGHT - 40)) + 20
    Asteroid(Ast).Dir = RND * PI2 '                                      random direction
    Asteroid(Ast).Speed = INT(RND * 6) + 2 '                             random speed
    Asteroid(Ast).Size = 2 ^ INT(RND * 3) '                              random size
NEXT Ast
SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) '                                  graphics screen
Spin = FALSE '                                                           no asteroid spin
DO '                                                                     begin main loop
   CLS '                                                                 clear screen
   _LIMIT 60 '                                                           60 frames per second
   LOCATE 2, 19: PRINT "Press the spacebar to activate asteroid spin" '  print directions
   IF _KEYHIT = 32 THEN Spin = NOT Spin '                                flip spin flag if spacebar pressed
   IF Spin THEN '                                                        spin asteroids?
       FOR Obj = 1 TO 10 '                                               yes, cycle through object points
            Object(Obj).Radian = Object(Obj).Radian + SPINRATE '         move radian location
           IF Object(Obj).Radian > PI2 THEN Object(Obj).Radian = Object(Obj).Radian - PI2 ' keep within limits
       NEXT Obj
   END IF
   FOR Ast = 1 TO MAXASTEROIDS '                                         cycle through asteroids
        Vector.x = SIN(Object(1).Radian) '                               calculate vector from 1st object point
        Vector.y = COS(Object(1).Radian)
        P1.x = Asteroid(Ast).Loc.x + Vector.x * Object(1).Radius * Asteroid(Ast).Size ' plot location on screen
        P1.y = Asteroid(Ast).Loc.y + Vector.y * Object(1).Radius * Asteroid(Ast).Size
       PSET (P1.x, P1.y) '                                               draw a pixel
       FOR Obj = 2 TO 10 '                                               cycle through remaining points
            Vector.x = SIN(Object(Obj).Radian) '                         calculate vector from object point
            Vector.y = COS(Object(Obj).Radian)
            Np.x = Asteroid(Ast).Loc.x + Vector.x * Object(Obj).Radius * Asteroid(Ast).Size ' plot location
            Np.y = Asteroid(Ast).Loc.y + Vector.y * Object(Obj).Radius * Asteroid(Ast).Size ' on screen
           LINE -(Np.x, Np.y) '                                          draw line from previous point
       NEXT Obj
       LINE -(P1.x, P1.y) '                                              draw final line back to start
        Vector.x = SIN(Asteroid(Ast).Dir) '                              get vector from asteroid radian
        Vector.y = COS(Asteroid(Ast).Dir)
        Asteroid(Ast).Loc.x = Asteroid(Ast).Loc.x + Vector.x * Asteroid(Ast).Speed ' plot location on screen
        Asteroid(Ast).Loc.y = Asteroid(Ast).Loc.y + Vector.y * Asteroid(Ast).Speed
       IF Asteroid(Ast).Loc.x < -19 THEN Asteroid(Ast).Loc.x = SWIDTH + 19 '        keep asteroids on screen
       IF Asteroid(Ast).Loc.x > SWIDTH + 19 THEN Asteroid(Ast).Loc.x = -19
       IF Asteroid(Ast).Loc.y < -19 THEN Asteroid(Ast).Loc.y = SHEIGHT + 19
       IF Asteroid(Ast).Loc.y > SHEIGHT + 19 THEN Asteroid(Ast).Loc.y = -19
   NEXT Ast
   _DISPLAY '                                                            update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                leave when ESC pressed
SYSTEM '                                                                 return to operating system
' Asteroid object data (10 coordinate vector points)
DATA 0,-5,5,-10,10,-5,7,0,10,5,2,10,-5,10,-10,5,-10,-5,-5,-10

Figure 7: Beware, falling rocks ahead

Each point on the asteroids was created from a combination of a radian and radius length values. Figure 8 below describes this process.

Figure 8: Storing an irregular shaped object

The asteroid object x,y vector points are stored in line 88 of the code as a DATA statement. The FOR ... NEXT statement in lines 36 through 40 READs each set of values in and then converts them to a radius (length) and radian value. The next FOR ... NEXT statement in lines 41 through 47 sets up the Asteroids() array that contains each asteroid's location ( Asteroid().Loc.x/y ), the radian direction of travel ( Asteroid().Dir ), the asteroid speed ( Asteroid().Speed ), and finally the size of each asteroid ( Asteroid().Size ).

This information is then used in the FOR ... NEXT loop contained in lines 61 through 83. Lines 62 through 66 takes the first set of values in Object(1), converts the radian to a vector quantity, and then calculates the position on screen and saving the results in P1.x and P1.y. The PSET statement is used to draw a pixel at the location as a place-holder for the LINE statements that will follow.

The FOR ... NEXT loop in lines 67 through 73 cycle's through the remaining Object(2-10) values. Just like the first point these values are used to create the remainder of the screen positions. This time however a LINE statement is used to continue from the last screen position used and "connect the dots".

Line 74 completes the outer perimeter of the asteroid by joining the last point back to the first with a LINE statement.

Lines 75 through 78 then creates a motion vector using the radian value found Asteroid().Dir and adds those vector quantities to the x and y locations of the asteroid. Lines 79 through 82 detect if the asteroid is leaving the screen and if so wraps it around to the opposite side.

If the user presses the space bar to active asteroid rotation the variable Spin is set to TRUE which then activates the IF statement in line 55.

The FOR ... NEXT statement in lines 56 through 59 cycles through each Object()'s radian value and adds the value in SPINRATE to it, effectively causing the radian to move clockwise ever so slightly. Line 58 ensures that if the radian value exceeds the value of 2 * PI the value is wrapped back around to the beginning of the radian range by subtracting 2 * PI.

It's an amazingly small piece of code that achieves so much. To keep the code to a minimum for the tutorial purposes however a trade off was made. The spin of each asteroid can't be independently controlled since there is only one master Object() array to work with. To allow for independent spin rates and directions for each asteroid a separate Object() array for would be needed for each asteroid significantly increasing the size of the code. This will also allow for a greater variety of asteroids instead of just the one depicted on screen but for display and learning purposes this example code gets the point across very well.

A Trigonometry Visual Aid

Some people learn best by reading and doing, others through lectures, and still others through visual instruction (like the author).  To this end the following program was created for the tutorial to help those who may benefit from seeing the actual math and graphics in motion in real time. The source code to the program TrigDemo.BAS can be found in your .\tutorial\Lesson17\ directory. You'll also need to make sure the files glinputtop.BI and glinput_noerror.BI are in the same directory as TrigDemo.BAS. The BI files are library add-ons needed for the program to run. (You'll learn about libraries in lesson 20.)

Figure 9: The relationship between radians, degrees, and vectors

Figure 10: Showing the relation between two objects

Figure 9 above shows the screen as it appears when you first run the program. The radian will sweep across the circle at first in an animated sequence. In the upper left hand corner you can enter a degree, radian, or vector and that graphical representation will appear within the circle along with all the math formulas filled out with the correct results.

Figure 10 above shows the second page. This page will allow you to enter two coordinate values and the relationships between them will be shown graphically within the grid window. The "from" coordinate is shown as the center of the circle and the "to" coordinate lies on the circle's perimeter. This relationship illustrates how the circle is still present as described on the first page.

This program also makes a handy little reference to have at your side to remember all the formulas and how to set them up.

Sprite Rotation

The previous asteroid example isn't the only way to rotate a set of points around a common center. Another method is to use something called a rotation matrix. A rotation matrix is an advanced trig concept but basically it's used to perform a rotation in a given space. This lesson is not going to go into rotation matrix detail but if you're interested you can visit this page to read more.

The rotation matrix converts to these four lines of code:

SINr! = SIN(-Angle! / 57.2957795131) ' get the sine of the radian
COSr! = COS(-Angle! / 57.2957795131) ' get the cosine of the radian
Vx! = x! * COSr! + SINr! * y! '        calculate new x vector
Vy! = y! * COSr! - x! * SINr! '        calculate new y vector

x! and y! are the vectors where the point currently lies. Angle! is the degree in which to rotate the x! and y! vectors toward. The calculation -Angle! / 57.2957795131 in the first two lines are converting the angle degree into a radian. Finally, Vx! and Vy! are the new vector coordinates for the point. These four lines will only work when it's assumed that the center of rotation is coordinate (0, 0)

In the next code example this rotation matrix is used to rotate the four corners of an image. Once rotated the _MAPTRIANGLE command is used to map the original image onto a new image surface using the rotated Vx! and Vy! coordinate locations.

Note: The four lines of code above were written by the original author of QB64, Rob (Galleon).

( This code can be found at .\tutorial\Lesson17\SpriteRotateDemo.bas )

'** Sprite Rotation Demo

CONST FALSE = 0, TRUE = NOT FALSE '    truth detectors
CONST SWIDTH = 640, SHEIGHT = 480 '    screen width and height
CONST GRAY = _RGB32(64, 64, 64) '      define colors
CONST WHITE = _RGB32(255, 255, 255)

TYPE XYPAIR '      x,y pair definition
    x AS SINGLE '  x value
    y AS SINGLE '  y value
END TYPE

DIM Img AS LONG '          original bee image
DIM RotImg AS LONG '       rotated image of bee
DIM Angle AS INTEGER '     current angle of rotation (degrees)
DIM FlyBeFree AS INTEGER ' TRUE if be is flying around
DIM Speed AS INTEGER '     speed of bee in flight
DIM Bee AS XYPAIR '        bee image x,y flight coordinates
DIM Vector AS XYPAIR '     bee image x,y vector values in flight
DIM SpinDir AS INTEGER '   rotation direction of image

SCREEN _NEWIMAGE(640, 480, 32) '                                        graphics screen
COLOR WHITE, GRAY '                                                     white text on gray background
Img = _LOADIMAGE(".\tutorial\Lesson13\tbee0.png", 32) '                 load bee image
Speed = 2 '                                                             set bee flight speed
DO '                                                                    begin main loop
   IF NOT FlyBeFree THEN '                                              is bee free to fly?
        Bee.x = 319 '                                                   center bee on screen
        Bee.y = 239
       DO '                                                             begin stationary bee loop
           _LIMIT 120 '                                                 120 frames per second
           CLS , GRAY '                                                 clear screen with gray background
           LOCATE 2, 22: PRINT "Press space bar to set the bee free." ' display instructions
           IF _KEYHIT = 32 THEN FlyBeFree = TRUE '                      if space bar pressed set flag
           LOCATE 5, 9: PRINT "Original Image"; '                       no, text above original image
           _PUTIMAGE (50, 50), Img '                                    display original image
            RotateImage Angle, Img, RotImg '                            rotate bee image
           _PUTIMAGE (Bee.x - _WIDTH(RotImg) / 2, Bee.y - _HEIGHT(RotImg) / 2), RotImg ' display bee
           LOCATE 21, 26: PRINT "Rotating one degree per frame." '      display info
            Angle = Angle + 1 '                                         increment rotation angle (degrees)
           IF Angle = 360 THEN Angle = 0 '                              reset angle when needed
           _DISPLAY '                                                   update screen with changes
       LOOP UNTIL FlyBeFree OR _KEYDOWN(27) '                           leave when bee freed or ESC pressed
   ELSE '                                                               yes, bee is free to fly
        Angle = 0 '                                                     reset angle
        SpinDir = 1 '                                                   clockwise spin (positive degrees)
       DO '                                                             begin flight loop
           _LIMIT 120 '                                                 120 frames per second
           CLS , GRAY '                                                 clear screen with gray background
           LOCATE 2, 24: PRINT "Press space bar to trap the bee." '     display instructions
            RotateImage Angle, Img, RotImg '                            rotate bee image
            Radian = _D2R(Angle) '                                      convert degree angle to radian
            Vector.x = SIN(Radian) '                                    calculate vectors from radian
            Vector.y = -COS(Radian)
            Bee.x = Bee.x + Vector.x * Speed '                          update bee location
            Bee.y = Bee.y + Vector.y * Speed
           _PUTIMAGE (Bee.x - _WIDTH(RotImg) / 2, Bee.y - _HEIGHT(RotImg) / 2), RotImg ' display bee
            Angle = Angle + SpinDir '                                   increment angle (degrees)
           IF ABS(Angle) = 360 THEN '                                   full circle reached?
                SpinDir = -SpinDir '                                    yes, reverse rotation
                Angle = 0 '                                             reset angle
           END IF
           _DISPLAY '                                                   update screen with changes
       LOOP UNTIL _KEYHIT = 32 OR _KEYDOWN(27) '                        leave when space bar or ESC pressed
        FlyBeFree = FALSE '                                             reset flag
        Angle = 0 '                                                     reset angle
   END IF
LOOP UNTIL _KEYDOWN(27) '                                               leave when ESC pressed
SYSTEM '                                                                return to operating system

'------------------------------------------------------------------------------------------------------------

SUB RotateImage (Degree AS SINGLE, InImg AS LONG, OutImg AS LONG)

    '** Rotates an image by the degree specified.
    '
    'NOTE: OutImg passed by reference is altered
    '
    'Degree - amount of rotation to add to image (-359.999.. - +359.999...) (INPUT)
    'InImg  - image to rotate (INPUT )
    'OutImg - rotated image   (OUTPUT)
    '
    '** This subroutine based on code provided by Rob (Galleon) on the QB64.NET website in 2009.
    '** Special thanks to Luke for explaining the matrix rotation formula used in this routine.

   DIM px(3) AS INTEGER '     x vector values of four corners of image
   DIM py(3) AS INTEGER '     y vector values of four corners of image
   DIM Left AS INTEGER '      left-most value seen when calculating rotated image size
   DIM Right AS INTEGER '     right-most value seen when calculating rotated image size
   DIM Top AS INTEGER '       top-most value seen when calculating rotated image size
   DIM Bottom AS INTEGER '    bottom-most value seen when calculating rotated image size
   DIM RotWidth AS INTEGER '  width of rotated image
   DIM RotHeight AS INTEGER ' height of rotated image
   DIM WInImg AS INTEGER '    width of original image
   DIM HInImg AS INTEGER '    height of original image
   DIM Xoffset AS INTEGER '   offsets used to move (0,0) back to upper left corner of image
   DIM Yoffset AS INTEGER
   DIM COSr AS SINGLE '       cosine of radian calculation for matrix rotation
   DIM SINr AS SINGLE '       sine of radian calculation for matrix rotation
   DIM x AS SINGLE '          new x vector of rotated point
   DIM y AS SINGLE '          new y vector of rotated point
   DIM v AS INTEGER '         vector counter

   IF OutImg THEN _FREEIMAGE OutImg '               free any existing image
    WInImg = _WIDTH(InImg) '                        width of original image
    HInImg = _HEIGHT(InImg) '                       height of original image
    px(0) = -WInImg / 2 '                                                  -x,-y ------------------- x,-y
    py(0) = -HInImg / 2 '             Create points around (0,0)     px(0),py(0) |                 | px(3),py(3)
    px(1) = px(0) '                   that match the size of the                 |                 |
    py(1) = HInImg / 2 '              original image. This                       |        .        |
    px(2) = WInImg / 2 '              creates four vector                        |       0,0       |
    py(2) = py(1) '                   quantities to work with.                   |                 |
    px(3) = px(2) '                                                  px(1),py(1) |                 | px(2),py(2)
    py(3) = py(0) '                                                         -x,y ------------------- x,y
    SINr = SIN(-Degree / 57.2957795131) '           sine and cosine calculation for rotation matrix below
    COSr = COS(-Degree / 57.2957795131) '           degree converted to radian, -Degree for clockwise rotation
   DO '                                             cycle through vectors
        x = px(v) * COSr + SINr * py(v) '           perform 2D rotation matrix on vector
        y = py(v) * COSr - px(v) * SINr '           https://en.wikipedia.org/wiki/Rotation_matrix
        px(v) = x '                                 save new x vector
        py(v) = y '                                 save new y vector
       IF px(v) < Left THEN Left = px(v) '          keep track of new rotated image size
       IF px(v) > Right THEN Right = px(v)
       IF py(v) < Top THEN Top = py(v)
       IF py(v) > Bottom THEN Bottom = py(v)

      v = v + 1 '                                   increment vector counter
   LOOP UNTIL v = 4 '                               leave when all vectors processed
    RotWidth = Right - Left + 1 '                   calculate width of rotated image
    RotHeight = Bottom - Top + 1 '                  calculate height of rotated image
    Xoffset = RotWidth \ 2 '                        place (0,0) in upper left corner of rotated image
    Yoffset = RotHeight \ 2
    v = 0 '                                         reset corner counter
   DO '                                             cycle through rotated image coordinates
        px(v) = px(v) + Xoffset '                   move image coordinates so (0,0) in upper left corner
        py(v) = py(v) + Yoffset
        v = v + 1 '                                 increment corner counter
   LOOP UNTIL v = 4 '                               leave when all four corners of image moved
    OutImg = _NEWIMAGE(RotWidth, RotHeight, 32) '   create rotated image canvas
    '                                               map triangles onto new rotated image canvas
   _MAPTRIANGLE (0, 0)-(0, HInImg - 1)-(WInImg - 1, HInImg - 1), InImg TO _
                (px(0), py(0))-(px(1), py(1))-(px(2), py(2)), OutImg
   _MAPTRIANGLE (0, 0)-(WInImg - 1, 0)-(WInImg - 1, HInImg - 1), InImg TO _
                (px(0), py(0))-(px(3), py(3))-(px(2), py(2)), OutImg

END SUB

'------------------------------------------------------------------------------------------------------------

Figure 11: Rotating the bee image and setting it free

It's important to remember that a sprite's dimensions, the width and height, will change as it transitions through the rotation. If you replace line 38 with this code:

_PUTIMAGE (Bee.x, Bee.y), RotImg ' image not centered

you'll be able to see the size differences as the image will appear to wobble as it is drawn. You always need to use the center point of a sprite you wish to rotate to ensure the rotation looks correct. Lines 38 and 57 correct this:

_PUTIMAGE (Bee.x - _WIDTH(RotImg) / 2, Bee.y - _HEIGHT(RotImg) / 2), RotImg ' display bee

by moving the image to the left half of it's width and up half of it's height to ensure that Bee.x and Bee.y are truly the center point of the sprite.

For the best possible sprite rotations it's always best to use sprite images that have widths and heights that are not divisible by 2 (odd numbers). Sprites with these dimensions will have a true center point to work with and rotate as smoothly as possible.

In the RotateImage subroutine this centering of the sprite also has to be taken into account. Lines 105 through 114 retrieves the width and height of the image passed in. Four vector pairs are then created, px(0),py(0) through px(3),py(3), that create an area equal to the size of the image that has the coordinate (0,0) as the center point. The rotation matrix algorithm needs the center point to be (0,0) to rotate the four vector quantities around the center.

Lines 115 and 116 pre-calculate the sine and cosine of the radius used to rotate the four points. Lines 117 through 127 then loops through all four vector pairs which applies the matrix rotation algorithm them in lines 118 and 119. Lines 122 through 125 record the lowest and highest values for x and y coordinates seen. As stated earlier the size of the rotated sprite will change and lines 122 through 125 record this change.

Lines 128 and 129 then calculates the new rotated image width and height dimensions and are used to create the new image holder for the rotated image in line 138.

The center point however is still coordinate (0,0) so all vector points need to be shifted to the right and down half of the image width and height to put coordinate (0,0) back into the upper left corner of the image. This is performed in lines 133 through 137.

Finally the original image needs to be mapped onto the new image using the new vector quantities as the four points for the image. The _MAPTRIANGLE statement is used to do this. Figure 12 below shows this process.

Figure 12: Using _MAPTRIANGLE

_MAPTRIANGLE is used to grab any three coordinate points of an image and then map the resulting triangular image to any three subsequent points. It can even handle 3D drawing. Check out the _MAPTRIANGLE Wiki page for more information and abilities of this statement.

As an added bonus since you know the degree angle of rotation for the sprite you can use that information to set a vector for sprite motion. Lines 52 through 56 takes the degree angle and converts it to a radian. Vector quantities are then created from the radian value and finally added to the bee's location multiplied by a desired speed. You can now set your rotated sprites free.

Here's some code that puts everything covered in this lesson into use.

( This code can be found at .\tutorial\Lesson17\ZombieRotate.bas )

'** Demo Zombie Animation With Rotation
'** Overhead zombie sprites provided by Riley Gombart at:
'** https://opengameart.org/content/animated-top-down-zombie
'** Sounds downloaded from The Sounds Resource at:
'** https://www.sounds-resource.com/pc_computer/plantsvszombies/sound/1430

TYPE XYPAIR '      x,y pair definition
    x AS SINGLE '  x value
    y AS SINGLE '  y value
END TYPE

CONST TRANSPARENT = _RGB32(255, 0, 255) ' transparent color

DIM ZombieSheet AS LONG '                 zombie sprite sheet
DIM Zombie(15) AS LONG '                  zombie sprites parsed from sheet
DIM Brains AS LONG '                      brain image
DIM Groan(6) AS LONG '                    zombie groaning sounds
DIM RotatedZombie AS LONG '               rotated zombie sprite
DIM Sprite AS INTEGER '                   sprite counter
DIM Angle2Brains AS SINGLE '              angle from zombie to brains
DIM Frame AS INTEGER '                    frame counter
DIM Zomby AS XYPAIR '                     zombie location
DIM Brain AS XYPAIR '                     brain location
DIM Vector AS XYPAIR '                    vector to brains

'** Load sprite sheet and parse out individual sprites and load zombie groan sounds

ZombieSheet = _LOADIMAGE(".\tutorial\Lesson17\zombie311x288.png", 32)
_CLEARCOLOR TRANSPARENT, ZombieSheet
FOR Sprite = 0 TO 15 '
    Zombie(Sprite) = _NEWIMAGE(311, 288, 32)
   _PUTIMAGE , ZombieSheet, Zombie(Sprite), (Sprite * 311, 0)-(Sprite * 311 + 310, 287)
   IF Sprite < 6 THEN
        Groan(Sprite + 1) = _SNDOPEN(".\tutorial\Lesson17\groan" + _TRIM$(STR$(Sprite + 1)) + ".ogg")
   END IF
NEXT Sprite
_FREEIMAGE ZombieSheet '                                          sprite sheet no longer needed
Brains = _LOADIMAGE(".\tutorial\Lesson17\brains.png", 32) '       load brain image
_CLEARCOLOR TRANSPARENT, Brains '                                 set brain image transparent color
SCREEN _NEWIMAGE(800, 600, 32) '                                  enter graphics screen
RANDOMIZE TIMER '                                                 seed random number generator
_MOUSEHIDE '                                                      hide mouse pointer
Zomby.x = 399 '                                                   position zombie
Zomby.y = 299
Sprite = 0 '                                                      reset sprite counter
DO '                                                              begin main loop
   _LIMIT 30 '                                                    30 frames per second
   CLS '                                                          clear screen
   WHILE _MOUSEINPUT: WEND '                                      get latest mouse information
    Brain.x = _MOUSEX '                                           record mouse location
    Brain.y = _MOUSEY
    Angle2Brains = P2PDegree(Zomby, Brain) '                      get angle from zombie to brains
    Degree2Vector Angle2Brains, Vector '                          convert angle to vector
    Frame = Frame + 1 '                                           increment frame counter
   IF Frame = 90 THEN '                                           90 frames gone by? (3 seconds)
        Frame = 0 '                                               yes, reset frame counter
       _SNDPLAY Groan(INT(RND(1) * 6) + 1) '                      play random zombie groan
   END IF
   IF Frame MOD 5 = 0 THEN '                                      frame evenly divisible by 5? (6 FPS)
        Sprite = Sprite + 1 '                                     yes, increment to next sprite in animation
       IF Sprite = 16 THEN Sprite = 0 '                           keep sprite number within limit
        RotateImage Angle2Brains, Zombie(Sprite), RotatedZombie ' rotate zombie sprite at same angle
   END IF
    Zomby.x = Zomby.x + Vector.x '                                update zombie location
    Zomby.y = Zomby.y + Vector.y '                                draw centered zombie and brains
   _PUTIMAGE (Zomby.x - _WIDTH(RotatedZombie) \ 2, Zomby.y - _HEIGHT(RotatedZombie) \ 2), RotatedZombie
   _PUTIMAGE (Brain.x - _WIDTH(Brains) \ 2, Brain.y - _HEIGHT(Brains) \ 2), Brains
   _DISPLAY '                                                     update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                         leave when ESC key pressed
FOR Sprite = 0 TO 15 '                                            cycle through zombie images (memory cleanup)
   _FREEIMAGE Zombie(Sprite) '                                    remove image from memory
   IF Sprite < 6 THEN _SNDCLOSE Groan(Sprite + 1) '               remove sound from memory
NEXT Sprite
_FREEIMAGE RotatedZombie '                                        remove image from memory
_FREEIMAGE Brains '                                               remove image from memory
SYSTEM '                                                          return to operating system

------------------------------------------------------------------------------------------------------------
SUB RotateImage (Degree AS SINGLE, InImg AS LONG, OutImg AS LONG)

    '** Rotates an image by the degree specified.
    'NOTE: OutImg passed by reference is altered
    'Degree - amount of rotation to add to image (-359.999.. - +359.999...) (INPUT)
    'InImg  - image to rotate (INPUT )
    'OutImg - rotated image   (OUTPUT)
    '** This subroutine based on code provided by Rob (Galleon) on the QB64.NET website in 2009.
    '** Special thanks to Luke for explaining the matrix rotation formula used in this routine.

   DIM px(3) AS INTEGER '     x vector values of four corners of image
   DIM py(3) AS INTEGER '     y vector values of four corners of image
   DIM Left AS INTEGER '      left-most value seen when calculating rotated image size
   DIM Right AS INTEGER '     right-most value seen when calculating rotated image size
   DIM Top AS INTEGER '       top-most value seen when calculating rotated image size
   DIM Bottom AS INTEGER '    bottom-most value seen when calculating rotated image size
   DIM RotWidth AS INTEGER '  width of rotated image
   DIM RotHeight AS INTEGER ' height of rotated image
   DIM WInImg AS INTEGER '    width of original image
   DIM HInImg AS INTEGER '    height of original image
   DIM Xoffset AS INTEGER '   offsets used to move (0,0) back to upper left corner of image
   DIM Yoffset AS INTEGER
   DIM COSr AS SINGLE '       cosine of radian calculation for matrix rotation
   DIM SINr AS SINGLE '       sine of radian calculation for matrix rotation
   DIM x AS SINGLE '          new x vector of rotated point
   DIM y AS SINGLE '          new y vector of rotated point
   DIM v AS INTEGER '         vector counter

   IF OutImg THEN _FREEIMAGE OutImg '               free any existing image
    WInImg = _WIDTH(InImg) '                        width of original image
    HInImg = _HEIGHT(InImg) '                       height of original image
    px(0) = -WInImg / 2 '                                                  -x,-y ------------------- x,-y
    py(0) = -HInImg / 2 '             Create points around (0,0)     px(0),py(0) |                 | px(3),py(3)
    px(1) = px(0) '                   that match the size of the                 |                 |
    py(1) = HInImg / 2 '              original image. This                       |        .        |
    px(2) = WInImg / 2 '              creates four vector                        |       0,0       |
    py(2) = py(1) '                   quantities to work with.                   |                 |
    px(3) = px(2) '                                                  px(1),py(1) |                 | px(2),py(2)
    py(3) = py(0) '                                                         -x,y ------------------- x,y
    SINr = SIN(-Degree / 57.2957795131) '           sine and cosine calculation for rotation matrix below
    COSr = COS(-Degree / 57.2957795131) '           degree converted to radian, -Degree for clockwise rotation
   DO '                                             cycle through vectors
        x = px(v) * COSr + SINr * py(v) '           perform 2D rotation matrix on vector
        y = py(v) * COSr - px(v) * SINr '           https://en.wikipedia.org/wiki/Rotation_matrix
        px(v) = x '                                 save new x vector
        py(v) = y '                                 save new y vector
       IF px(v) < Left THEN Left = px(v) '          keep track of new rotated image size
       IF px(v) > Right THEN Right = px(v)
       IF py(v) < Top THEN Top = py(v)
       IF py(v) > Bottom THEN Bottom = py(v)
        v = v + 1 '                                 increment vector counter
   LOOP UNTIL v = 4 '                               leave when all vectors processed
    RotWidth = Right - Left + 1 '                   calculate width of rotated image
    RotHeight = Bottom - Top + 1 '                  calculate height of rotated image
    Xoffset = RotWidth \ 2 '                        place (0,0) in upper left corner of rotated image
    Yoffset = RotHeight \ 2
    v = 0 '                                         reset corner counter
   DO '                                             cycle through rotated image coordinates
        px(v) = px(v) + Xoffset '                   move image coordinates so (0,0) in upper left corner
        py(v) = py(v) + Yoffset
        v = v + 1 '                                 increment corner counter
   LOOP UNTIL v = 4 '                               leave when all four corners of image moved
    OutImg = _NEWIMAGE(RotWidth, RotHeight, 32) '   create rotated image canvas
    '                                               map triangles onto new rotated image canvas
   _MAPTRIANGLE (0, 0)-(0, HInImg - 1)-(WInImg - 1, HInImg - 1), InImg TO _
                (px(0), py(0))-(px(1), py(1))-(px(2), py(2)), OutImg
   _MAPTRIANGLE (0, 0)-(WInImg - 1, 0)-(WInImg - 1, HInImg - 1), InImg TO _
                (px(0), py(0))-(px(3), py(3))-(px(2), py(2)), OutImg

END SUB
'------------------------------------------------------------------------------------------------------------
SUB Degree2Vector (D AS SINGLE, V AS XYPAIR)

    '** NOTE: V passed by reference is altered
    '** Degree to Vector Calculator
    '** Converts the supplied degree to a normalized vector
    ' D        - degree            (INPUT )
    ' V.x, V.y - normalized vector (OUTPUT)
    ' .017453292 = PI / 180

   DIM Degree AS SINGLE ' the degree value passed in

    Degree = D '                         don't alter passed in value
   IF Degree < 0 OR Degree >= 360 THEN ' degree outside limits?
        Degree = FixRange(Degree, 360) ' yes, correct degree
   END IF
    V.x = SIN(Degree * .017453292) '     return x vector
    V.y = -COS(Degree * .017453292) '    return y vector

END SUB
'------------------------------------------------------------------------------------------------------------
FUNCTION P2PDegree (P1 AS XYPAIR, P2 AS XYPAIR)

    '** Point to Point Degree Calculator
    '** Returns the degree from point 1 to point 2 with 0 degrees being up
    ' P1.x, P1.y - FROM coordinate (INPUT)
    ' P2.x, P2.y - TO   coordinate (INPUT)
    ' 57.29578 = 180 / PI

   DIM Theta AS SINGLE ' the returned degree Degree

   IF P1.y = P2.y THEN '                                  do both points have same y value?
       IF P1.x = P2.x THEN '                              yes, do both points have same x value?
           EXIT FUNCTION '                                yes, identical points, no degree (0)
       END IF
       IF P2.x > P1.x THEN '                              is second point to the right of first point?
            P2PDegree = 90 '                              yes, the degree must be 90
       ELSE '                                             no, second point is to the left of first point
            P2PDegree = 270 '                             the degree must be 270
       END IF
       EXIT FUNCTION '                                    leave function, degree calculated
   END IF
   IF P1.x = P2.x THEN '                                  do both points have the same x value?
       IF P2.y > P1.y THEN '                              yes, is second point below first point?
            P2PDegree = 180 '                             yes, the degree must be 180
       END IF
       EXIT FUNCTION '                                    leave function, degree calculated
   END IF
    Theta = _ATAN2(P2.y - P1.y, P2.x - P1.x) * 57.29578 ' calculate +/-180 degree Degree
   IF Theta < 0 THEN Theta = 360 + Theta '                convert to 360 degree
    Theta = Theta + 90 '                                  set 0 degrees as up
   IF Theta > 360 THEN Theta = Theta - 360 '              adjust accordingly if needed
    P2PDegree = Theta '                                   return degree (0 to 359.99..)

END FUNCTION
'------------------------------------------------------------------------------------------------------------

Figure 13: They're coming to get you Barbara!

This example makes use of sprites, a sprite sheet, sounds, and animation. Looks like a good start to a Zombie stalker game. Move the brain around on the screen with the mouse and watch the Zombie hungerly chase it. Again, it's amazing how little code is required to create something like this.