Lesson 8: Variable Arrays
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.
Note: Because of the length of this code and the fact we are not going to save it the source code has been included in the tutorial asset file named BadCircles.BAS. Just go ahead and load that code into your IDE now (unless you really want to type the following program in which I highly commend you for!).
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 and save it as GoodCircles.BAS when finished. We'll go over how this code can be so much smaller yet do the exact same thing.
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
CONST TOTAL = 50
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
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. Save the code as OneDimension.BAS when finished.
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" ' this goes into row 0
User$(1) = "Mark" ' this goes into row 1
User$(2) = "John" ' this goes into row 2
and so on ...
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:
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. Save the code as TwoDimension.BAS when completed.
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)
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 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 and save it as TypeDemo.BAS when finished.
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
Pixel.y = 239
Pixel.c = _RGB32(255, 255, 255)
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.c ' 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:
Xlocation AS INTEGER
Ylocation AS INTEGER
Color AS _UNSIGNED LONG
Now your code is even more self documenting:
Pixel.Xlocation = 319
Pixel.Ylocation = 239
Pixel.Color = _RGB32(255, 255, 255)
PSET (Pixel.Xlocation, Pixel.Ylocation), Pixel.Color
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. Save this code as TypeDemo2.BAS.
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 and save it as TypeCircles.BAS when finished.
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 basically seeing here is the basis of most video games.
- Clear the screen
- Update the object positions in the array
- Check for interaction of objects (coming soon in upcoming lesson)
- Draw objects onto the screen based on current array information
- Rinse and repeat over and over
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 1MB, 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.
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 and save it as Groceries.BAS when finished.
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
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
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)
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:
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.
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.