Lesson 8: Variable Arrays

Before starting this lesson this would be a good time to work the through the IDE side lesson. You've now gotten to a point where the features of the IDE will greatly enhance your coding efficiency and help you with writing great code. Think of the IDE as your coding partner when creating QB64 source code and knowing how to properly interact with your partner is key.

Games typically have many objects on the screen all moving independent of each other. Up to this point the example programs you have encountered have controlled one object on the screen. If for instance you want to control 5 circles on the screen you would need to create separate variables for each x,y coordinate pair of all 5 circles. That means 10 individually named variables simply to locate 5 circles on the screen. Then you would need another 10 variables to control the x,y movement, another 5 variables for the size of the circles, and yet another 5 variables for the color of each circle! Imagine if you want to control 100 circles at a time ... yes that would require 600 separate variables to keep track of! Enter the following example program that illustrates just how much work it is to control 5 simple circles on the screen at once using separate variables.

Don't bother typing the following example in. Simply load it from the tutorial directory. 

( This code can be found at .\tutorial\Lesson8\BadCircles.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST RED = _RGB32(127, 0, 0) '     define 5 colors
CONST GREEN = _RGB32(0, 127, 0)
CONST BLUE = _RGB32(0, 0, 127)
CONST PURPLE = _RGB32(127, 0, 127)
CONST CYAN = _RGB32(0, 127, 127)

DIM x1!, y1!, size1% ' circle 1 x,y coordinates, radius
DIM x2!, y2!, size2% ' circle 2 x,y coordinates, radius
DIM x3!, y3!, size3% ' circle 3 x,y coordinates, radius
DIM x4!, y4!, size4% ' circle 4 x,y coordinates, radius
DIM x5!, y5!, size5% ' circle 5 x,y coordinates, radius
DIM xdir1!, ydir1! '          x,y direction of circle 1
DIM xdir2!, ydir2! '          x,y direction of circle 2
DIM xdir3!, ydir3! '          x,y direction of circle 3
DIM xdir4!, ydir4! '          x,y direction of circle 4
DIM xdir5!, ydir5! '          x,y direction of circle 5

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                  enter graphics screen
RANDOMIZE TIMER '                                 seed number generator
size1% = INT(RND(1) * 20) + 20 '                  random circle 1 size
size2% = INT(RND(1) * 20) + 20 '                  random circle 2 size
size3% = INT(RND(1) * 20) + 20 '                  random circle 3 size
size4% = INT(RND(1) * 20) + 20 '                  random circle 4 size
size5% = INT(RND(1) * 20) + 20 '                  random circle 5 size
x1! = INT(RND(1) * (639 - size1% * 2)) + size1% ' random circle 1 x coordinate
x2! = INT(RND(1) * (639 - size2% * 2)) + size2% ' random circle 2 x coordinate
x3! = INT(RND(1) * (639 - size3% * 2)) + size3% ' random circle 3 x coordinate
x4! = INT(RND(1) * (639 - size4% * 2)) + size4% ' random circle 4 x coordinate
x5! = INT(RND(1) * (639 - size5% * 2)) + size5% ' random circle 5 x coordinate
y1! = INT(RND(1) * (479 - size1% * 2)) + size1% ' random circle 1 y coordinate
y2! = INT(RND(1) * (479 - size2% * 2)) + size2% ' random circle 2 y coordinate
y3! = INT(RND(1) * (479 - size3% * 2)) + size3% ' random circle 3 y coordinate
y4! = INT(RND(1) * (479 - size4% * 2)) + size4% ' random circle 4 y coordinate
y5! = INT(RND(1) * (479 - size5% * 2)) + size5% ' random circle 5 y coordinate
DO
    xdir1! = RND(1) - RND(1) '                    random circle 1 x direction
LOOP UNTIL xdir1! <> 0 '                          loop back if it's 0
DO
    xdir2! = RND(1) - RND(1) '                    random circle 2 x direction
LOOP UNTIL xdir2! <> 0 '                          loop back if it's 0
DO
    xdir3! = RND(1) - RND(1) '                    random circle 3 x direction
LOOP UNTIL xdir3! <> 0 '                          loop back if it's 0
DO
    xdir4! = RND(1) - RND(1) '                    random circle 4 x direction
LOOP UNTIL xdir4! <> 0 '                          loop back if it's 0
DO
    xdir5! = RND(1) - RND(1) '                    random circle 5 x direction
LOOP UNTIL xdir5! <> 0 '                          loop back if it's 0
DO
    ydir1! = RND(1) - RND(1) '                    random circle 1 y direction
LOOP UNTIL ydir1! <> 0 '                          loop back if it's 0
DO
    ydir2! = RND(1) - RND(1) '                    random circle 2 y direction
LOOP UNTIL ydir2! <> 0 '                          loop back if it's 0
DO
    ydir3! = RND(1) - RND(1) '                    random circle 3 y direction
LOOP UNTIL ydir3! <> 0 '                          loop back if it's 0
DO
    ydir4! = RND(1) - RND(1) '                    random circle 4 y direction
LOOP UNTIL ydir4! <> 0 '                          loop back if it's 0
DO
    ydir5! = RND(1) - RND(1) '                    random circle 5 y direction
LOOP UNTIL ydir5! <> 0 '                          loop back if it's 0

DO '                                              begin main program loop
   CLS '                                          clear the screen
   _LIMIT 240
    x1! = x1! + xdir1! '                          change circle 1 x location
   IF x1! < size1% OR x1! > 639 - size1% THEN '   did circle 1 hit side of screen?
        xdir1! = -xdir1! '                        yes, reverse circle 1 x direction
   END IF
    x2! = x2! + xdir2! '                          change circle 2 x location
   IF x2! < size2% OR x2! > 639 - size2% THEN '   did circle 2 hit side of screen?
        xdir2! = -xdir2! '                        yes, reverse circle 2 x direction
   END IF
    x3! = x3! + xdir3! '                          change circle 3 x location
   IF x3! < size3% OR x3! > 639 - size3% THEN '   did circle 3 hit side of screen?
        xdir3! = -xdir3! '                        yes, reverse circle 3 x direction
   END IF
    x4! = x4! + xdir4! '                          change circle 4 x location
   IF x4! < size4% OR x4! > 639 - size4% THEN '   did circle 4 hit side of screen?
        xdir4! = -xdir4! '                        yes, reverse circle 4 x direction
   END IF
    x5! = x5! + xdir5! '                          change circle 5 x location
   IF x5! < size5% OR x5! > 639 - size5% THEN '   did circle hit side of screen?
        xdir5! = -xdir5! '                        yes, reverse circle 5 x direction
   END IF
    y1! = y1! + ydir1! '                          change circle 1 y location
   IF y1! < size1% OR y1! > 479 - size1% THEN '   did circle 1 hit side of screen?
        ydir1! = -ydir1! '                        yes, reverse circle 1 y direction
   END IF
    y2! = y2! + ydir2! '                          change circle 2 y location
   IF y2! < size2% OR y2! > 479 - size2% THEN '   did circle 2 hit side of screen?
        ydir2! = -ydir2! '                        yes, reverse circle 2 y direction
   END IF
    y3! = y3! + ydir3! '                          change circle 3 y location
   IF y13 < size3% OR y3! > 479 - size3% THEN '   did circle 3 hit side of screen?
        ydir3! = -ydir3! '                        yes, reverse circle 3 y direction
   END IF
    y4! = y4! + ydir4! '                          change circle 4 y location
   IF y4! < size4% OR y4! > 479 - size4% THEN '   did circle 4 hit side of screen?
        ydir4! = -ydir4! '                        yes, reverse circle 4 y direction
   END IF
    y5! = y5! + ydir5! '                          change circle 5 y location
   IF y5! < size5% OR y5! > 479 - size5% THEN '   did circle 5 hit side of screen?
        ydir5! = -ydir5! '                        yes, reverse circle 5 y direction
   END IF
   CIRCLE (x1!, y1!), size1%, RED '               draw circle 1
   PAINT (x1!, y1!), RED, RED '                   paint circle 1
   CIRCLE (x2!, y2!), size2%, GREEN '             draw circle 2
   PAINT (x2!, y2!), GREEN, GREEN '               paint circle 2
   CIRCLE (x3!, y3!), size3%, BLUE '              draw circle 3
   PAINT (x3!, y3!), BLUE, BLUE '                 paint circle 3
   CIRCLE (x4!, y4!), size4%, PURPLE '            draw circle 4
   PAINT (x4!, y4!), PURPLE, PURPLE '             paint circle 4
   CIRCLE (x5!, y5!), size5%, CYAN '              draw circle 5
   PAINT (x5!, y5!), CYAN, CYAN '                 paint circle 5
   _DISPLAY '                                     update screen with changes
LOOP UNTIL _KEYDOWN(27) '                         leave loop when ESC key pressed
SYSTEM '                                          return to Windows

Figure 1: That's a LOT of code for 5 moving circles!

Imagine a game like Asteroids that keeps track of the player's ship, a UFO, bullets from the player's ship and UFO, and upwards of 50 asteroids on the screen at any given time. The individual variables to handle all of this motion would be mind boggling. Also keep in mind that Asteroids was written on hardware in 1980 that only had 8KB of RAM. This web page alone is going to me more than 50KB! So there must be a better, more memory efficient way of handling all of these variables, correct?

The answer is yes and it's done with a construct known as an array. Below is the same 5 circle code but this time written with arrays. Enter the code into your IDE. We'll go over how this code can be so much smaller yet do the exact same thing.

( This code can be found at .\tutorial\Lesson8\GoodCircles.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST TOTAL = 5 '                number of circles on screen at one time

DIM x!(TOTAL), y!(TOTAL) '       circle x,y coordinates
DIM Xdir!(TOTAL), Ydir!(TOTAL) ' circle x,y directions
DIM Size%(TOTAL) '               circle sizes
DIM Col~&(TOTAL) '               circle colors
DIM Count% '                     generic FOR...NEXT counter

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                                                 enter graphics screen
RANDOMIZE TIMER '                                                                seed number generator
FOR Count% = 1 TO TOTAL '                                                        cycle through circles
    Size%(Count%) = INT(RND(1) * 20) + 20 '                                      random circle size
    Col~&(Count%) = _RGB32(INT(RND(1) * 256), INT(RND(1) * 256), INT(RND(1) * 256)) ' random color
    x!(Count%) = INT(RND(1) * (639 - Size%(Count%) * 2)) + Size%(Count%) '       random x location
    y!(Count%) = INT(RND(1) * (479 - Size%(Count%) * 2)) + Size%(Count%) '       random y location
    Xdir!(Count%) = RND(1) - RND(1) '                                            random x direction
    Ydir!(Count%) = RND(1) - RND(1) '                                            random y direction
NEXT Count%
DO '                                                                             begin main program loop
   CLS '                                                                         clear the screen
   _LIMIT 240 '                                                                  240 frames per second
   FOR Count% = 1 TO TOTAL '                                                     cycle through circles
        x!(Count%) = x!(Count%) + Xdir!(Count%) '                                change circle x location
       IF x!(Count%) < Size%(Count%) OR x!(Count%) > 639 - Size%(Count%) THEN '  circle hit screen edge?
            Xdir!(Count%) = -Xdir!(Count%) '                                     yes, reverse x direction
       END IF
        y!(Count%) = y!(Count%) + Ydir!(Count%) '                                change circle y location
       IF y!(Count%) < Size%(Count%) OR y!(Count%) > 479 - Size%(Count%) THEN '  circle hit screen edge?
            Ydir!(Count%) = -Ydir!(Count%) '                                     yes, reverse y direction
       END IF
       CIRCLE (x!(Count%), y!(Count%)), Size%(Count%), Col~&(Count%) '           draw circle
       PAINT (x!(Count%), y!(Count%)), Col~&(Count%), Col~&(Count%) '            paint inside circle
   NEXT Count%
   _DISPLAY '                                                                    update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                        leave loop when ESC pressed
SYSTEM '                                                                         return to Windows

Much less code but the same result! But wait, there's more. Since arrays were used to build the code a simple change can have a huge effect. Change line 5 of the code from this:

CONST TOTAL = 5 '                number of circles on screen at one time

to this:

CONST TOTAL = 50 '               number of circles on screen at one time

and then run the code again. By changing this one value we resized the arrays to handle even more objects at once. The power of arrays can't be understated and it's key that you understand their use if you wish to become a game programmer.

Figure 2: Holy circles Batman!

One Dimensional Arrays - DIM Revisited

Of all the concepts in programming it seems that arrays are amongst the most troubling of ideas to understand for new programmers. An array can be defined as "A systematic arrangement of objects, usually in rows and columns." It's the "rows and columns" portion of the definition that makes arrays so powerful in the hands of a programmer. Arrays allow the programmer to keep track of hundreds, thousands, even millions of variables with a predefined table of values that are easily accessible by name, number, or both. If you are familiar with the concept of spreadsheets (Microsoft Excel for example) then you already know what an array is. Arrays contain cells within rows and columns and each cell contains a piece of data we can store, change, or retrieve by referencing the cell's location using row and column numbers. An array is nothing more than a spreadsheet.

The simplest array that can be created is the one dimensional array that contains a single column of information with a predefined number of rows. Type in the following snippet of code to see a one dimensional array in use.

( This code can be found at .\tutorial\Lesson8\OneDimension.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM User$(5) ' array to hold up to 6 strings (0 to 5)
DIM Count% '   generic counter used in FOR...NEXT loop

'----------------------------
'- Main Program Begins Here -
'----------------------------

User$(0) = "Terry" '     assign an element to each array index
User$(1) = "Mark"
User$(2) = "John"
User$(3) = "Mary"
User$(4) = "Luke"
User$(5) = "Melissa"

FOR Count% = 0 TO 5 '    loop through all 6 array indexes
   PRINT User$(Count%) ' display element of each array index
NEXT Count%

Up to this point we've been using the DIM statement to declare variables for use in our programs. However, the DIM statement is also used to create variables that have dimension, or more than one element associated with them. An element is a string or numeric value that can be stored within the array. User$ is the variable name of the array created in the example code and each element within the array can be accessed by providing an index in the form of a numeric value. The User$ array was created using an index value of 5 allowing for 6 string elements (0 through 5) to be stored within it. An array such as User$ is known as a one dimensional array because it sets up one column where information can be stored. An easy way to visualize this is to view it as a spreadsheet as seen in Figure 3 below.

Figure 3: User$(5)

To place elements, or information, into the array you simply address the array variable name with an index number pointing to the row where the element is to be stored. This portion of the example code:

User$(0) = "Terry" '     assign an element to each array index
User$(1) = "Mark"
User$(2) = "John"
User$(3) = "Mary"
User$(4) = "Luke"
User$(5) = "Melissa"

placed the elements into their corresponding rows by addressing each row as an index number. The end result can be seen in the populated spreadsheet shown in Figure 4 below.

Figure 4: User$(5) Populated

By using an index number with the array variable name you can obtain the value of the element at that index location like so:

PRINT User$(2)

This will result in the string John printed to the screen. Likewise, if you wish to change the value of any element simply use the index number and assign a new value.

User$(2) = "Thomas"

This will overwrite the element's value at array index 2 with the new information as seen in Figure 5 below.

Figure 5: Element at index 2 altered

Two Dimensional Arrays

Two dimensional arrays allow you to create a table that can be accessed in two dimensions meaning that the array has multiple rows and columns. Type in the following example code to see a two dimensional array in use.

( This code can be found at .\tutorial\Lesson8\TwoDimension.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM Contacts$(3, 5) ' two dimensional array used as contact database
DIM Match% '          contains index number of match if found
DIM Count% '          index counter
DIM SearchName$ '     name user wishes to search for

'----------------------------
'- Main Program Begins Here -
'----------------------------

Contacts$(1, 1) = "Mike Smith" '     populate database with information
Contacts$(1, 2) = "123 Any Street"
Contacts$(1, 3) = "Anytown"
Contacts$(1, 4) = "OH"
Contacts$(1, 5) = "12345"

Contacts$(2, 1) = "Laura Flowers"
Contacts$(2, 2) = "456 This Street"
Contacts$(2, 3) = "Toledo"
Contacts$(2, 4) = "MA"
Contacts$(2, 5) = "23432"

Contacts$(3, 1) = "Tom Thumb"
Contacts$(3, 2) = "765 My Street"
Contacts$(3, 3) = "Mayberry"
Contacts$(3, 4) = "NC"
Contacts$(3, 5) = "24241"

DO '                                                                      main loop begins
   PRINT '                                                                blank line
   PRINT "Enter a name, or partial name, to search for." '                display instructions
   INPUT "Enter nothing to end the program > ", SearchName$ '             get name to search for
   IF SearchName$ = "" OR SearchName$ = "nothing" THEN END '              end program if nothing
    Count% = 1 '                                                          reset index counter
    Match% = 0 '                                                          reset match indicator
   DO '                                                                   begin search loop
       IF INSTR(UCASE$(Contacts$(Count%, 1)), UCASE$(SearchName$)) THEN ' is name contained within?
            Match% = Count% '                                             yes, remember this index
           EXIT DO '                                                      leave the DO...LOOP
       END IF
        Count% = Count% + 1 '                                             increment index counter
   LOOP UNTIL Count% = 4 '                                                leave loop when value 4
   IF Match% THEN '                                                       was there a match? (<> 0)
       PRINT '                                                            yes, blank line
       PRINT "Match found!" '                                             inform user
       PRINT '                                                            blank line
       PRINT "Name   : "; Contacts$(Match%, 1) '                          display element values
       PRINT "Address: "; Contacts$(Match%, 2)
       PRINT "City   : "; Contacts$(Match%, 3)
       PRINT "State  : "; Contacts$(Match%, 4)
       PRINT "Zip    : "; Contacts$(Match%, 5)
   ELSE '                                                                 no, nothing matched
       PRINT '                                                            blank line
       PRINT "Match not found." '                                         inform user
   END IF
LOOP '                                                                    loop back to beginning

Figure 6: A simple contact database program

This example program creates a two dimensional array called Contacts$ that contains 4 rows (0 to 3) and 6 columns (0 to 5) by issuing the statement in line 5:

DIM Contacts$(3, 5) ' two dimensional array used as contact database

It's important not to forget that array indexes start at zero but that does not mean you need to fill the elements with data starting at index zero. Many times it just feels correct to start using an array at index one, such as this example program does. This is purely up to the programmer whether index zero is used or not. Once again by visualizing the array as a spreadsheet it may make more sense as to how the data is being stored.

Figure 7: Contacts$(3,5) populated

In this array column 1 is the "name", column 2 is the "address", column 3 is the "city", column 4 is the "state", and column 5 is the "zip code". To get the zip code for "Laura Flowers" from the array you could issue the command:

PRINT Contacts$(2, 5)

The 2 refers to the row number and the 5 refers to the column number. The programmer would need to remember which column number contains the correct data he/she is looking for.

However, what if instead of remembering that column 1 is the "name", column 2 is the "address", and so on we could simply ask for the "name" in row 3? There is a way to do this with TYPE definitions!

TYPE Definitions

TYPE definitions allow you to define a data structure of grouped and related information. Let's start off by creating a simple variable that has a TYPE definition attached to it. Type the following code in.

( This code can be found at .\tutorial\Lesson8\TypeDemo.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

TYPE DOT
    x AS INTEGER '            x location of pixel
    y AS INTEGER '            y location of pixel
    color AS _UNSIGNED LONG ' color of pixel
END TYPE

DIM Pixel AS DOT ' a variable created with a TYPE definition

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '       enter graphics screen
Pixel.x = 319 '                        set pixel x coordinate
Pixel.y = 239 '                        set pixel y coordinate
Pixel.color = _RGB32(255, 255, 255) '  set pixel color (white)
PSET (Pixel.x, Pixel.y), Pixel.color ' draw pixel on screen

In this program a type definition data structure called DOT was created using the TYPE statement in line 5. The END TYPE statement in line 9 denotes where the data structure ends. Inside the data structure are where variables get defined as seen in lines 6 through 8. However variable type identifiers such as the percent sign ( % ) for integer and a dollar sign ( $ ) for strings are not allowed. Variable types must be declared using the AS clause with the name of the variable type spelled out after it. A listing of data type names used with the AS clause can be found here in the QB64 Wiki.

Once a type definition data structure has been defined it must be associated with a variable name in order to use it. In line 11 of the example code the type definition was attached to a variable named Pixel.

DIM Pixel AS DOT ' a variable created with a TYPE definition

Pixel can now be used to access the data structure DOT embedded within it. In order to access a data structure sub-variable the use of a period ( . ) must follow the main variable name and then the name of the sub-variable as seen in lines 18 through 20 in the example code.

Pixel.x = 319 '                        set pixel x coordinate
Pixel.y = 239 '                        set pixel y coordinate
Pixel.color = _RGB32(255, 255, 255) '  set pixel color (white)

To get an idea of what this structure looks like let's again view the variable in spreadsheet form.

Figure 8: Pixel's structure

As you can see in figure 8 above the use of type definitions creates self-documenting column names within the variable itself. This makes retrieving information from a variable much more intuitive as line 21 in the example code highlights.

PSET (Pixel.x, Pixel.y), Pixel.color ' draw pixel on screen

The line of code documents itself: "Draw a point at the pixel's x and y points with the pixel color". If you really wanted the code to be even more explanatory you could have prepared your TYPE statement like so:

TYPE DOT
    Xlocation AS INTEGER '    x location of pixel
    Ylocation AS INTEGER '    y location of pixel
    Color AS _UNSIGNED LONG ' color of pixel
END TYPE

Now your code is even more self documenting:

Pixel.Xlocation = 319 '                                set pixel x coordinate
Pixel.Ylocation = 239 '                                set pixel y coordinate
Pixel.color = _RGB32(255, 255, 255) '                  set pixel color (white)
PSET (Pixel.Xlocation, Pixel.Ylocation), Pixel.color ' draw pixel on screen

A one dimensional array created with a type definition structure takes on two dimensional array properties. Let's modify the previous example program to use a one dimensional array.

( This code can be found at .\tutorial\Lesson8\TypeDemo2.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST MAXPIXELS = 10 '         number of pixels +1 on screen

TYPE DOT
    x AS INTEGER '             x location of pixel
    y AS INTEGER '             y location of pixel
    c AS _UNSIGNED LONG '      color of pixel
END TYPE

DIM Pixel(MAXPIXELS) AS DOT '  create one dimension array with data structure
DIM Count% '                   index counter

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                              enter graphics screen
RANDOMIZE TIMER '                                             seed random number generator
FOR Count% = 0 TO MAXPIXELS '                                 cycle through all indexes
    Pixel(Count%).x = INT(RND(1) * 640) '                     random x between 0 and 639
    Pixel(Count%).y = INT(RND(1) * 480) '                     random y between 0 and 479
    Pixel(Count%).c = _RGB32(255, 255, 255) '                 set pixel color (white)
   PSET (Pixel(Count%).x, Pixel(Count%).y), Pixel(Count%).c ' turn pixel on
NEXT Count%

Using the value defined in the constant MAXPIXELS we can now create as many pixels on the screen as we wish. Using the current value of 10 let's again visualize the array that was created by using a spreadsheet.

Figure 9: 1D array with 2D properties

The index number serves as the row value and the type definition data structure names represent the columns. If you need to pull the y value of the 4th pixel you could simply issue the command:

PRINT Pixel(3).y ' the value 14 printed to the screen

Let's take GoodCircles.BAS that we wrote at the beginning of this lesson and modify it to use a type definition data structure. Type the following code into your IDE.

( This code can be found at .\tutorial\Lesson8\TypeCircles.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST TOTAL = 50 '              number of circles on screen at one time

TYPE CIRCLES '                  data structure to hold circle info
    x AS SINGLE '               x location of circle
    y AS SINGLE '               y location of circle
    r AS INTEGER '              radius of circle
    c AS _UNSIGNED LONG '       color of circle
    Xdir AS SINGLE '            x direction of circle
    Ydir AS SINGLE '            y direction of cicle
END TYPE

DIM Circles(TOTAL) AS CIRCLES ' structured array to hold circles
DIM Count% '                    generic FOR...NEXT counter

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                                                 enter graphics screen
RANDOMIZE TIMER '                                                                seed number generator
FOR Count% = 0 TO TOTAL '                                                        cycle through circles
    Circles(Count%).r = INT(RND(1) * 20) + 20 '                                  random circle size
    Circles(Count%).c = _RGB32(INT(RND(1) * 256), INT(RND(1) * 256), INT(RND(1) * 256)) ' random color
    Circles(Count%).x = INT(RND(1) * (639 - Circles(Count%).r * 2)) + Circles(Count%).r ' random x location
    Circles(Count%).y = INT(RND(1) * (479 - Circles(Count%).r * 2)) + Circles(Count%).r ' random y location
    Circles(Count%).Xdir = RND(1) - RND(1) '                                     random x direction
    Circles(Count%).Ydir = RND(1) - RND(1) '                                     random y direction
NEXT Count%
DO '                                                                             begin main program loop
   CLS '                                                                         clear the screen
   _LIMIT 240 '                                                                  240 frames per second
   FOR Count% = 0 TO TOTAL '                                                     cycle through circles
        Circles(Count%).x = Circles(Count%).x + Circles(Count%).Xdir '           change circle x location
       IF Circles(Count%).x < Circles(Count%).r OR Circles(Count%).x > 639 - Circles(Count%).r THEN 'edge?
            Circles(Count%).Xdir = -Circles(Count%).Xdir '                       yes, reverse x direction
       END IF
        Circles(Count%).y = Circles(Count%).y + Circles(Count%).Ydir '           change circle y location
       IF Circles(Count%).y < Circles(Count%).r OR Circles(Count%).y > 479 - Circles(Count%).r THEN 'edge?
            Circles(Count%).Ydir = -Circles(Count%).Ydir '                       yes, reverse y direction
       END IF
       CIRCLE (Circles(Count%).x, Circles(Count%).y), Circles(Count%).r, Circles(Count%).c ' draw circle
       PAINT (Circles(Count%).x, Circles(Count%).y), Circles(Count%).c, Circles(Count%).c '  paint inside circle
   NEXT Count%
   _DISPLAY '                                                                    update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                        leave loop when ESC pressed
SYSTEM '                                                                         return to Windows

When you execute the program you get the exact same outcome as before and yes the code is a bit longer. However, in the previous code we wrote at the beginning of the lesson there were 6 separate arrays that needed to be tracked and updated all with different names to remember. By using the type definition in our new code there is only one array to track, Circles, that was created in line 16 of the code.

DIM Circles(TOTAL) AS CIRCLES ' structured array to hold circles

It's much easier to work with one array than it is to work with 6 arrays to get the exact same outcome. This one dimensional array also has two dimensional properties as seen in the spreadsheet representation below in Figure 10.

Figure 10: The Circles() structured array

The example code is updating values in the spreadsheet based on what is happening on the screen. The first FOR...NEXT loop populates the array with starting values. The second FOR...NEXT loop embedded within the main program DO...LOOP updates the circle's positions on the screen. Those updated positions are then saved back to the spreadsheet to keep track of each and every circle. The next time through the loop the process is done again. This table of data is used to keep track of all moving objects on the screen, their position, their direction, their color, and the radius of each circle. Are you starting to understand how a game like Asteroids can have up to 50 asteroids flying around on the screen now?

What you are seeing here is the basis of most video games.

Three Dimensional Arrays

If one and two dimensional arrays were difficult to envision then three dimensional arrays takes this to a new level. Think of a three dimensional array as a stack of two dimensional arrays. For example, take the following line of code:

DIM Calendar$(11, 6, 5)

This line of code sets up 12 individual spreadsheets (0 to 11), each containing 7 columns (0 to 6) and 6 rows (0 to 5). Do you see what was created here? It's a one year calendar. Again, this will be easier to envision as spreadsheets.

Figure 11: Calendar created with a three dimensional array

Did you notice that we addressed the rows and columns differently in this array than previous ones? In this case the first index (11) contains the 12 months. The second index (6) is addressing each column in the array and the third index (5) is addressing each row in the array. In all the previous examples we addressed the row first and the column second, but this is completely up to the programmer as to how the elements are arranged and used. It's all in how you "envision" using your array. With a situation such as a calendar it makes sense to address the column first (the week day) and then the row (the week).

Three dimensional arrays can get quite complex and are rarely needed but can be quite useful. In fact, one of the few times I've ever needed one in my career was to create exactly what is seen here, a calendar, for a drug testing program I wrote for a local municipality back in the early 1990's. Normally I simply rely on structured one or two dimensional arrays using type definitions.

Four Dimensional Arrays and Beyond

QuickBasic allowed for up to 60 dimensions to be used with arrays! QB64 goes even further than that allowing for as many dimensions as your computer's memory has space for. For example if you create a four dimensional array of integers like so:

DIM MyArray%(10, 20, 30, 40)

You would need 11 x 21 x 31 x 41 cells to hold them equaling a total of 293,601 cells! In QB64 an integer occupies 4 bytes of space so multiplying 293,601 x 4 gives us a grand total of 1,174,404 bytes, or a little over one megabyte of RAM needed to create this array in memory. I've personally never had a need for an array larger than three dimensions but I have seen code that deals with scientific issues go beyond even four dimensional arrays.

The only example I can think of for a four dimensional array would be to expand on the three dimensional calendar we created.

DIM Calendar$(999, 11, 6, 5)

This would create a 1000 year calendar, or 1000 yearly 3 dimensional calendar arrays stacked on top of one another creating a total of 504,000 cells! Yikes!

Resizing Arrays: The REDIM Statement

All of the arrays we have created up to this point have been static arrays. Static arrays have a predefined size by using DIM and a fixed index value.

DIM MyList$(10)

MyList$ is a static array with a fixed value of 11 indexes available (0 - 10). If you were to try and enter a value into MyList$ at index number 11:

MyList$(11) = "Milk"

the IDE will happily let you do it. However when you execute the program this will happen:

Figure 12: Oops

"Subscript out of range" means that the index number of 11 that was used is not within the range of 0 through 10 assigned to the array. What if an array needs to be larger than originally planned? We kind of addressed this earlier with the use of a constant at the beginning of our code. In the TypeCircles.BAS example this line was at the top of the code:

CONST TOTAL = 50

and if we need more than 50 bouncing circles we can simply change this number accordingly. That's fine during compile time but what about cases during run-time when the user needs the array to be larger? It's not like the user can get into the code, change a variable, recompile the code and continue happily on their way. This is where the REDIM statement comes in. If you create an array using REDIM you can resize the array later on inside the code. REDIM creates what's known as a dynamic array.

This is best shown with some example code. Type the following program in.

( This code can be found at .\tutorial\Lesson8\Groceries.bas )

'---------------------
'- Declare variables -
'---------------------

TYPE GROCERIES '               grocery list structure
    Item AS STRING '           grocery item description
    Price AS STRING '          grocery item price
END TYPE

REDIM MyList(0) AS GROCERIES ' grocery list array
DIM Item$ '                    grocery item
DIM Index% '                   current array index
DIM Total! '                   total price of groceries

'----------------
'- Main program -
'----------------

PRINT "              My Grocery List" '                   print instructions
PRINT "       -----------------------------"
PRINT " Enter nothing in description when finished."
PRINT
Index% = 0 '                                              initialize variables
Total! = 0
DO '                                                      begin main program loop
   LINE INPUT "Description: "; Item$ '                    get grocery item
   IF Item$ <> "" THEN '                                  was an item entered?
        Index% = Index% + 1 '                             yes, increment array index number
       REDIM _PRESERVE MyList(Index%) AS GROCERIES '      resize the array and preserve values
        MyList(Index%).Item = Item$ '                     add grocery item to array
       LINE INPUT "Item Price : "; MyList(Index%).Price ' get grocery item price
        Total! = Total! + VAL(MyList(Index%).Price) '     add item price to total
   END IF
LOOP UNTIL Item$ = "" '                                   leave loop when no more items
PRINT '                                                   print master grocery list
PRINT "Here is the list of items you need:"
PRINT "-----------------------------------"
FOR Index% = 1 TO UBOUND(MyList) '                        loop up to upper boundary of array
   PRINT MyList(Index%).Item '                            print item at current index
NEXT Index%
PRINT "-----------------------------------"
PRINT "You'll also need $"; Total! '                      display total cost to user

Figure 13: Looks like someone is fixin' breakfast

There's three new statements in play here that we'll need to cover, REDIM, _PRESERVE, and UBOUND. Let's start off with REDIM in line 10 of the example program.

REDIM MyList(0) AS GROCERIES ' grocery list array

Using REDIM declares the MyList array as being dynamic, meaning the number of indexes can be changed within the source code. Here MyList has been declared with 0 as an index effectively making this an array with only one index available. If an item is entered by the user then the IF...THEN statement in line 27 becomes true and the code block is entered. An integer variable named Index% is incremented by 1. This will be the new amount to REDIM the MyList array with. Line 29 is where the magic happens.

REDIM _PRESERVE MyList(Index%) AS GROCERIES '      resize the array and preserve values

When increasing a dynamic array's size the default behavior of BASIC is to clear all previous contents of the array (the array's contents basically gets erased from RAM). However, in our code the user will be entering items over and over and we need to preserve that data instead of erasing it. This is what the optional _PRESERVE statement does. It directs REDIM to resize the array but keep the values that are already saved within the elements. Without the optional _PRESERVE statement the array would get resized but the existing data would be destroyed.

Since Index% has increased in value that new value will be used as the new index size to recreate the dynamic array. Each time the user enters an item the array is increased by one index. This ensures that the array is only as big as the user needs.

UBOUND is a statement that returns a numeric value equal to the upper boundary, or highest index number currently available, in an array. In line 38 of the code:

FOR Index% = 1 TO UBOUND(MyList) '                        loop up to upper boundary of array

UBOUND is returning the highest index number contained within the MyList array. If the user would have entered 7 items then UBOUND would return the value of 7.

Note: It's important to remember that UBOUND does not return the total number indexes but instead the highest one that exists. If the user did in fact enter 7 items then there are actually 8 indexes available (0 through 7) with UBOUND returning the largest index number of 7, not the total number of indexes which would be 8.

Another thing to note is that it's possible to create arrays that have a start index other than zero. For example, these arrays are perfectly acceptable to QB64:

DIM Numbers%(-50 TO 50) '         101 total indexes
DIM Temperature!(32 TO 212) '     180 total indexes
REDIM Kelvin!(-459 TO -150) '     309 total indexes
DIM MyArray&(10 TO 20, 8 TO 14) ' 60 total indexes

Therefore a counterpart to UBOUND exists named LBOUND that returns the lower index boundary number within an array.

Dynamic arrays are handy for situations where you don't know the exact number of indexes you'll need for a given situation and in situations where index numbers are never the same from one instance to the next. When we get into particle fountains in a later lesson you'll see where dynamic arrays really shine.

There is a downside to using dynamic arrays however and that is speed. Static arrays are much faster than dynamic arrays. Because static arrays never change in size their footprint can be pre-calculated in RAM and forgotten about with the code only having to deal with setting and retrieving element values. Dynamic arrays on the other hand need constant monitoring because at any moment they may need resizing and preserving. This causes a lot of overhead in your code which slows these types of arrays down.

Speaking of starting index values there is one more item to discuss concerning arrays. When you create a simple array like:

DIM MyArray%(10)

It's implied that the array index starts at 0 and ends at 10. However it's possible to force simple arrays to start with the value of 1 using the statement:

OPTION BASE 1

This statement is called a compiler directive and must be used in your code before any DIM or REDIM statements are created. This forces all all arrays created that were not explicitly told their starting index value to begin with 1. Some programmers don't like having their arrays start with 0 feeling that 1 is a better option. It's totally up to you whether you use OPTION BASE 1 or not. I will state though that most other languages do not offer this and their arrays always start with 0, so starting your arrays with 0 may be a good habit to stick with.

Your Turn

Modify the TypeCircles.BAS source code to create the following program as shown in Figure 14 below.

Figure 14: Adding and subtracting circles

- The program starts out with one bouncing circle on the screen.
- When the UP ARROW key is pressed circles are added.
- When the DOWN ARROW key is pressed circles are subtracted.
- There can be no less than 1 circle and no greater than 500 circles on the screen at a time.
- Use a dynamic array to hold the circle information.
- The dynamic array should adjust in size according to the number of circles needed to be shown.
- The directions and total counter should always be on top of the circles as seen in Figure 14 above.
- Create a subroutine called MakeCircle() that adds a circle to the dynamic array when needed.
- Save the code as AddCircles.BAS when completed.

Click here to see the solution.

Commands and Concepts Learned