The Fig-Forth v2 compiler was written in part to give the programmer a clean and easy to use graphics interface to the IBM system hardware. To this end a great deal of research and code fragments have been collected, tested and later implemented within the compiler, allowing a broad range of modes and hardware to be addressed. Not every card and monitor combination are fully supported at this time for all possible modes of operation, though the basic interface is sufficient for most DOS applications. Operation under any version of Windows is not guaranteed.
Graphics under the standard system is a result of two operations in the video hardware, specifically the tasks of selecting a display color and a raster position for that color to appear. For the purposes used in this chapter the color to be displayed is a function of the video hardware palette, while the raster position is selected by a location inside the video RAM segment. Thus the following definitions are used throughout this text;
RASTER
This term is the combined screen image in number of columns (pixels) and number of lines, or the whole of the display taken as a unit. Under specific circumstances this term will apply to a single scan line as it appears on the screen or is contained in memory, under the terms of "raster line" or "scan line."
PIXEL
This term is the smallest point of light that can appear on the monitor's video screen, without reference to its apparent color.
INDEX
This term is defined as the numeric or byte value to be placed into the video RAM, or an offset address applied to the video hardware. For all cases of either use in this section, the term shall apply to values in the range of 0-255.
SCAN LINE
This term is the width of the video screen in pixels or bytes, or a reference to a collection of such pixels on a single horizontal pass of the video electron beam.
COLOR
This term is the apparent visual image of the displayed pixel as it is seen on the monitor screen, and the needed data to define this color. (Explained below.)
REGISTER
This term is a control port within the video controller chip, generally specified as existing at a particular hexadecimal address.
BANK
Finally, this term is defined as a block of video memory, whether sequentially accessed or by special addressing. (Explained below.)
The video display process is a highly organized sequence of events meant to communicate between two very different devices; the computer itself which operates digitally and the monitor which is analog. Depending upon the monitor, video card and graphics mode selected these events may or may not mimic the operations present in a television receiver, which is the basis for a video display of any kind. The events that occur in such a process are comprised of a blanking signal, a number of scan lines worth of useful data, then another blanking signal and a vertical retrace. While a discussion of these events is beyond the scope of this manual two incidents are important to us, the retrace signals and the scan lines worth of data.
This Vertical Retrace event and Forth word marks the moment in time when the electron beam on the video display is returning to the top of the display field, in preparation for another pass over the screen to paint the data once more. Because the video display is composed of phosphors this activity is continuous, to keep the image being displayed lighted so it can be viewed. In most monitors this retrace period occurs between 50 to 80 times per second, depending on the mode selected and the hardware requirements.
This function is similar to VSYNC above, performing a delay until the next line of the display field is about to begin. Because lines occur faster than vertical framing, this event occurs between 15,000 and 40,000 times per second.
This is the primary function of the video display, that of creating images for the user to view. Each combined picture is a collection of individual pixels placed one after the other upon a scan line, and collections of these scan lines form frames or whole images. Of chief concern to the programmer is the memory arrangement of these scan lines and the pixels they carry, which is achieved by two distinct methods.
1. Direct Correspondence
This is the most simple interface available from the CRT hardware, in that each pixel is contained inside the video RAM segment one after the other. This mode of addressing is typically used in the HGR and HGR13 modes available in Fig-Forth, however the HGR mode requires multiple RAM banks to contain all the required pixels for the display. (See Figure 1.)
For the programmer the single segment mode offers the greatest ease of interface, however the number of pixels is limited to 64k. This mode is the BIOS 13 Hex foundation, the one called by HGR13. While this 320 pixel by 200 line mode is a favorite for doing many different types of programming, it does not allow the use of video RAM beyond the single segment addressed. The bank access mode is the one selected by the HGR functions in Fig-Forth, or 640 pixels by 480 lines and up. However, because each bank of RAM listed above is still a single segment of 64k, three of the scan lines on the page "jump" between banks at odd locations. Fig-Forth v2 corrects for this change and selects the proper memory bank for each pixel plotted, using (at most) the first 2.5 megabytes of memory available from the video hardware. While it is recognized that modern cards have considerably more RAM the interface for this is not included at this time, owing to the variety of methods of access used by various manufacturers. (Not including the speed-up routines required to move all those pixels about!)
2. Planar Access
As an alternative Fig-Forth also contains the necessary drivers to implement the planar model of video card memory access, in which each pixel across the screen line hangs upon another block of video RAM within the interface area. (Figure 2.)
In this mode the scan line consists of rapidly intermixed pixels coming from the selected memory locations, typically all of the four RAM segments defined up above. This mode is often called "unchained" or "bit mapped" in that each pixel must be mapped into a particular memory location, either upon a different memory segment or in a location within the bytes themselves. Fig-Forth v2 offers two representations of the bit mapped operation, the MGR word for mode 12 hex graphics and the 13 hex extended modes. In MGR mode the correspondence of any pixel on the screen is a little strange, a combination of bank access and memory location. So for the purposes of this manual we shall not address it. (And because it's only 16 color.)
By far the widest variety of video modes possible in Fig-Forth is that of the mode 13x variants, which combine the timings involved in several other modes with the unchained memory map shown in figure 2. Each of these modes use a maximum 256k memory available from the card, yet because the screen is now stretched across four physical banks of memory these modes offer us the possibility of multiple pages.
How this operates is really quite easy, given the example shown in Figure 1. Since it takes exactly 64k pixels to fill the screen as shown in the Single Segment diagram, if we accessed four banks of memory as shown in figure 2 we'd have four times the RAM required. In addition, by using some of the special functions in Fig-Forth we can access all of this available memory for graphics, allowing ourselves to define these pages and paint them invisibly before their display. (See Working with the graphics display.)
The final quality to be considered is that of color, or how a pixel appears on the screen at the time it is viewed. For every mode available in Fig-Forth except the one of MGR, this color selection consists of a single byte entry, which is stored into the video RAM area for its appearance in the given scan line. Being exactly one byte this means that any pixel can appear as one of 256 total colors, as defined by the system palette when it is viewed.
The Palette itself is a table of registers inside the video controller hardware, each of which defines how the pixel image appears on the screen;
So, though each pixel itself is an index to a collection of 256 current colors, the actual range of these colors can be defined by the program at hand. For most standard video cards the width of the palette registers is 6 binary bits, meaning that valid values are from 0 to 63. This allows us 63 shades of Red, 63 of Green and 63 shades of Blue all within a single pixel, which defines up to 256 colors out of a possible 262,144. With proper care and careful shading the combined image can appear very full bodied, because the human eye cannot distinguish the difference between the minor values. (Especially while the images are moving around.)
The first consideration in programming graphics in Fig-Forth is selecting which video mode is required by the program proposed. For most cases this will be either an HGR, HGR13X or two of the HGR13? modes, typically mode 1 or 2. The following table defines all possible modes of operation;
calling word | parameter | Width | Height | Colors | Pages | Size |
HGR13? | 0 -- |
320 | 175 | 256 | 4.681 | 14000 |
HGR13? | 1 -- |
320 | 200 | 256 | 4.096 | 16000 |
HGR13 | -none- |
320 | 200 | 256 | 1 | 64000 |
HGR13? | 2 -- |
320 | 240 | 256 | 3.413 | 19200 |
HGR13? | 3 -- |
320 | 350 | 256 | 2.340 | 28000 |
HGR13? | 4 -- |
320 | 400 | 256 | 2.048 | 32000 |
HGR13? | 5 -- |
320 | 480 | 256 | 1.707 | 38400 |
HGR13? | 6 -- |
360 | 175 | 256 | 4.161 | 15750 |
HGR13? | 7 -- |
360 | 200 | 256 | 3.640 | 18000 |
HGR13? | 8 -- |
360 | 240 | 256 | 3.034 | 21600 |
HGR13? | 9 -- |
360 | 350 | 256 | 2.080 | 31500 |
HGR13? | 10 -- |
360 | 400 | 256 | 1.820 | 36000 |
HGR13X | -none- |
360 | 480 | 256 | 1.517 | 43200 |
MGR | -none- |
640 | 480 | 16 | 1 | N/A |
HGR | -none- |
640 | 480 | 256 | 4.267 | N/A |
HGR1 | -- f | 800 | 600 | 256 | 3.413 | N/A |
HGR2 | -- f | 1024 | 768 | 256 | 2.667 | N/A |
HGR3 | -- f | 1280 | 1024 | 256 | 2 | N/A |
Functions returning a status flag are False if the mode ENGAGED properly with the current card and monitor combination, the number of effective pages limited by the amount of RAM available to the video processor. HGR13? modes of 1 and 2 are commonly used by game designers, for they mimic the appearance of HGR13 while containing multiple pages. Mode 2 is of particular interest in these applications, for the pixels on the screen are nearly square and a line stepping in both the X and Y directions appears as a 45 degree angle. In addition, mode 2 also has 40 lines of "extra" pixels to contain a status screen of the player's action, with the standard 200 lines above the fixed window. Modes of HGR13X, MGR and the HGR's are well suited to Graphical User Interfaces, where their higher pixel width and line count allow for greater detail. (NOTE: Page selection in HGR modes is limited to 2048 lines, the size of the internal address table.)
The second consideration for graphics applications is the number of pages to be employed by the program, a factor of how quickly the image on the screen will need to be updated. More often than not this will require a bit of thinking on the part of the programmer, because each mode offers limited paging. (See table.)
The third quality for consideration is how the screen will be used; will there be scrolling and in which direction, will a status screen be required with or without multiple pages, and will the program use the mouse as an interface. Each of these choices determine which will be the best mode to select for the given program, and how each of Forth's settings will be determined. For example, let's assume we're doing a program that requires the 320 by 200 line mode with multiple pages;
Example 1. Multiple pages in 320 by 200
The best mode for this kind of interface is that of HGR13? mode 1, which offers us 4.096 pages worth of data in the video memory. This mode is ideal for creating a double buffer entry, or an invisible page to be drawn while the user is viewing a previous version. It is common in this type of situation to maintain three copies of the video picture; a "clean background" page, a page with painted sprites for the user to view, and a hidden page that the program is creating while the user views the finished copy. Using this arrangement and the data from the above table this defines the video area as in the diagram shown. The gray areas represent those portions of the video memory not currently used in this scheme, with a deliberate "hole" between the pages to allow for writing sprites and making collision detection. While this is not the only arrangement that will fill the needs of the desired program, this is common practice for such programming.
Using this arrangement is very easy within the Fig-Forth version 2's operating environment, because Fig-Forth has been constructed with this kind of problem in its design. The first function to be defined is the location of these multiple pages and their relative offsets, using the information in the above table as the basis for our calculations;
0 VARIABLE PAGE1
0 VARIABLE PAGE2
0 VARIABLE CLEAN
16000 CONSTANT PAGE-SIZE
Note that for these variables we have set all three pages currently at zero, but we need to define the gaps that lay between the pages. For our purposes here we'll be using a very small gap because we want to keep things tiny, so we will define the gap as being two lines in size.
Now using this data we can compute the gap offset given that we are in a four bank mode and the horizontal resolution is 320 pixels; so dividing 320 by 4 gives us 80 bytes per screen line. Twice this value becomes the size of our gap area;
320 4 / 2 * CONSTANT GAP
And now we can define the starting locations of our screens;
PAGE-SIZE GAP + DUP PAGE2 ! DUP + CLEAN !
Now each variable above contains the relative offset of each video page, and we're ready to compute their offsets in terms of X and Y. Doing this is equally as easy, because we've already made the choices above for our definition;
0 0 2VARIABLE SCREEN1
0 200 2 + 2VARIABLE SCREEN2
0 400 4 + 2VARIABLE BACKGROUND
Naturally, all of these values can be specified as direct constants as shown below, but since the compiler is fully capable of performing the arithmetic for us at the time of loading there's little reason to do this.
0 VARIABLE PAGE1
16160 VARIABLE PAGE2
32320 VARIABLE CLEAN
0 0 2VARIABLE SCREEN1
0 202 2VARIABLE SCREEN2
0 404 2VARIABLE BACKGROUND
Next, we need a function to switch between these different screens, and a method for creating graphics upon all of them. This is where the kernel routines of Fig-Forth come into play, using the highly adaptive graphics interface unit;
0 VARIABLE WHICHPAGE
: FLIP WHICHPAGE @ IF PAGE2 ELSE PAGE1 THEN
@ VSTART WHICHPAGE 1 TOGGLE ;
This routine will automatically switch between the two display pages, updating the video starting location and the control bit required. How it works is very simple; the byte offset address of the page is sent to VSTART, which determines where the video card begins displaying pixels. Note that for HGR1, HGR2 and HGR3 modes the value of VSTART is a double word, the pixel column and the top line of the display area, using VESA to set the hardware registers of the card.
At this point we require a word to point specifically to the clean page, and a function to do the reverse. The reason is that we intend to copy background material from this clean page to erase our sprites before we draw them upon the invisible one, so we require a method to address it;
: >BACK BACKGROUND TO VOFFSET ;
: >PAGE WHICHPAGE @ IF SCREEN2 ELSE SCREEN1 THEN
TO VOFFSET ;
The value of VOFFSET is a double word that defines the line and pixel offset applied to Forth's graphic routines, automatically changing the location of where Forth believes the display page is located. Since Fig-Forth v2 allows up to 1024 lines to be applied in this manner it can cover about 1 and a quarter complete segments of video memory as defined by the hardware, or about 328k of memory if the card allowed it in this mode. However, having done this the graphics routines will always point to the correct page within the video memory, such that we can draw, query or print text into any location on these pages.
Now the program comes down to defining our sprites and building the proper database, then using our GETPIC and PUTPIC words to sample the background and place the new sprite upon the page;
: DO-SPRITE ( adr w h x y -- ) 2OVER 2OVER PAD 5 -ROLL
>BACK GETPIC 2OVER 2OVER PAD 5 -ROLL PUTPIC
>PAGE PUTPIC PUTPIC ;
How this word works takes a little explaining, but not that much given the ease of the words. It makes a copy of the four parameters on the stack that define the location and size of the graphic, (width, height, X and Y) inserts the PAD address under them, then copies the background for that area into the user memory. After duplicating the location and size again it then pastes the background on the invisible page, followed by the graphic specified in the address area when the word was executed. Note that in using PUTPIC with the VMODE control byte set to 4 (not included in this example) means that any pixel defined as a Zero is not copied to the screen page, so the actual image of the sprite does not need to contain a background. (And the background area should never contain a Zero pixel.) See the Advanced Input & Output section of the Programmer's Reference for programming the video mode byte.
Once all of our sprites are updated on the page we simply call our FLIP function above, and our main loop can call DO-SPRITE to repeat the process for the next page.
One of the first changes we can add to this program design is the quality of scrolling, or moving the display page around in virtual space. However, now we enter the realm of where certain video cards can create a problem for us, in that if we scrolled very far downward the card may not return to the top of the video memory space as older cards do. The same thing happens when we scroll upwards too far, for then the offset values become negative and difficult to control. However, this is simply a matter of redefining the video space as shown in the next diagram;
Using this arrangement is really quite simple, we define within the video memory an area called a Copy Region which limits the distance that any page can be advanced. However, to make our lives a bit more complex this also requires that we copy data from the Copy Region to the relative top of the video area, such that when any page covers the copy region we can flip back to the top. Fortunately, the mode we've selected allows us four distinct pages plus 19 extra lines, a value arrived at by dividing 64k by the line width times the height of the screen, then taking the remaining number of bytes and converting them to lines. Thus given our arrangement of 202 lines per screen times 4 with 819.2 lines possible, this leaves 11 lines left over to form a screen bottom. Now we need to define the top of the Copy Region, with a special consideration to the problem we're trying to avoid;
PAGE-SIZE GAP + 10 80 * + CLEAN @ + CONSTANT COPYR
This creates a constant to point to the Copy Region, so that we know when to flip to the top and start our scroll again. Our special condition is the one defined up above, for we cannot allow any page to approach the copy region without signaling that it has done so;
COPYR PAGE-SIZE - GAP - CONSTANT IN-COPY
This constant will be used to determine if any page has approached the Copy Region by testing the page tail, so that we know it's time to copy from the invisible pages to the top of the screen ram.
Next, we have to update the Video Table in Forth to allow us to plot one line ahead of any page as we proceed in the scroll, then update the copies of this "extra line" to the other two. At the end of this process we then perform the scroll function, either positive or negative as the case may be.
201 VTBL !
Setting this value will allow Forth to plot one line just ahead of the current screen area, just as it does for any other line. After doing this the value of MAXY will return the new entry, so all loops that use this value will follow our desire.
: COPY> ( -- ) PAD 320 1 0 201 GETPIC ;
This routine will retrieve a single line at the bottom of the currently defined page, which will be used to update the other two and the starting location. Finally, we need a destination for the copy region data as defined in yet another variable;
0 0 2VARIABLE COPYD
Now we need a support routine for managing the three pages, or a routine to give us the address of the highest page in the memory space;
: MAX-PAGE ( -- adr )
PAGE1 @ PAGE2 @ -- get first two values
2DUP U> IF DROP -- compare & keep #1 if greater
CLEAN @ U> -- compare result to clean
IF PAGE1 ELSE -- if larger get the address
CLEAN THEN -- else get clean address
ELSE NIP CLEAN @ U> -- else keep #2 & compare to clean
IF PAGE2 ELSE -- if page #2 still largest, return it
CLEAN THEN -- else background address
THEN ;
This routine will return the address pointing to the highest page offset, whichever it happens to be at the time of calling. We can now define our scroll down function, given the values created by our limits.
: +80 ( adr -- ) DUP @ 80 + DUP COPYR = IF DROP 0 -- add a line and
DUP COPYD ! THEN SWAP ! ; -- zero destination if needed
: +SCROLL PAGE1 +80 PAGE2 +80 CLEAN +80 1 SCREEN1 +! -- advance all
1 SCREEN2 +! 1 BACKGROUND +! MAX-PAGE -- pointers & get max.
@ IN-COPY U> IF 1 COPYD +! THEN ; -- test for copy & set dest.
These two routines will take care of the downward scroll effect, advancing the memory pointer locations for each page as the collection moves deeper in memory. Note that the scroll function only advances the copy destination pointer when a page hits the region, and when a page is "flipped" back to the top the destination is set to zero along with the page offset.
Now comes that all important double copy routine, which must copy not only the clean background line as it is constructed in front of the pages as they roll, but must also add a copy of that line to the top area of RAM so the scroll above can flip back to the top;
: DOUBLE-COPY >BACK COPY> -- get line from background area
>PAGE PAD 320 1 0 201 PUTPIC -- make invisible copy
WHICHPAGE 1 TOGGLE -- flip to display page
>PAGE PAD 320 1 0 201 PUTPIC -- make 2nd copy
WHICHPAGE 1 TOGGLE -- flip back to hidden page
MAX-PAGE @ IN-COPY U> IF -- test for copy region
COPYD TO VOFFSET -- set copy destination
PAD 320 1 0 201 PUTPIC -- make extra copy
THEN ;
Our scroll downward is now complete, a completely automatic process of advancing pages and pointers to set the process into motion. All we do now is call the process in the proper order, such as pseudo code given down below;
: MAIN >BACK DRAW-BACKGROUND DOUBLE-COPY
ERASE-SPRITES +SCROLL DRAW-SPRITES FLIP
DETECT-HITS ;
Of special note in this process is the position of the erase and draw sprites routines in the above code, because the background must be free to move behind the figures moving on the screen. In addition, because the old page (the one which was last viewed by the user) is a line behind the new one, the sprites need to be surrounded by an empty line and pixel frame or have their old positions recorded. This is especially true if the sprites are moving quickly, such as defining each sprite in the following manner;
( SPRITE DATA STRUCTURE )
54 ARRAY SPRITE -- sprite data area
0 +OFF .X-POS -- present position
2 +OFF .Y-POS -- present line
2 +OFF .OLD-X -- last plotted position
2 +OFF .OLD-Y -- last plotted line
2 +OFF .SPEED-X -- speed of object
2 +OFF .SPEED-Y -- speed of gravity
2 +OFF .IMAGE# -- which picture to use
2 +OFF .HITS -- damage they've taken
2 +OFF .IMAGE -- the image itself
NAE @ 36 + +CONSTANT .SIZE -- the total sprite size
And so on. When sprites are advanced they copy their old positions into the save locations, then advance themselves with the speed values. If the plan is to scroll up the copy region and top of RAM reverse roles in the above routines, with the copy detection and scroll values adjusted.
Scrolling left or right offers its own problem when working in the graphics mode, in that the video card bears the same line width as the effective screen. While there are ways around this if the host program is expected to contain a great deal of movement, Fig-Forth's relative plotting code often makes this practice unnecessary. While the methods involved in changing the hardware line is beyond the scope of this section, the following terms and definitions remain in effect;
First, the CRT hardware is designed to facilitate a pixel by pixel scrolling within its control registers, which selects which bank of memory is used for the starting plane of the image. (See diagram 2 in Theory section.) however the scope of this scroll is limited to the four banks available, meaning the display screen can exist in column 0, 1, 2, or 3. This is easily achieved by the Fig-Forth word VPAN, using the X offset value stored in VOFFSET;
VOFFSET 2+ @ VPAN
This code fragment will transfer the current plotting offset in pixels to the video hardware, aligning the drawing space to the same position as the display page. Thanks to Fig-Forth's internal plotting functions any image draw on the screen will be tracked perfectly to the moving page, while the background area slides behind the new sprites. Using both this operation and the definitions above a pan right function might appear as follows;
: PAN-RIGHT VOFFSET 2+ -- point to X offset
DUP @ 1+ DUP MAXX = IF -- test against max
DROP 0 1 VOFFSET +! THEN -- move back & adjust Y
DUP 4 MOD 0= IF PAGE1 -- test for bank rolling
1 OVER +! @ VSTART THEN -- advance start address if needed
SWAP PLEAT ! VPAN ; -- finish up and move pages
This process looks more complex than it truly is, for all we're doing is advancing the virtual page first, then adjusting the offsets for that page when the pan runs out of banks from which to select. However, even though this process will work it offers its own problem when executing the VSTART change, namely that the screen has been moved more than four columns;
Notice the right edge of this picture, in particular the line between the blue sky and lighter blue background, and the lower right hand corner. This is how the standard screen would appear after stepping the VSTART location downward by 1 byte, or rolling the screen to the right by 4 pixels. (This example uses highlighted colors, and assumes that white has been painted into the video memory ahead of the picture pane.) Thus the above routine should include a copy function much like the process discussed in vertical scrolling, such that all four columns in the direction of the scroll are updated when the pan runs out of banks to select. (The area where VSTART is being called above.) As one can expect all the copying concerns and multiple pages mentioned under vertical scrolling also apply when panning horizontally.
As a first program for working with graphics mode we're going to build a simple 3D projection system, one which merely implements two pages to allow for flipping between frames to create smooth animation. As can be drawn from the prior discussion this is a simple matter of setting up the page locations and then drawing our figures, initiating a flip when the drawing is complete.
0 VARIABLE PAGE1
19200 CONSTANT PAGE-SIZE
PAGE-SIZE VARIABLE PAGE2
As before the first step is to define our page locations, using the video mode we've decided to select. Since we want two pages so we can flip between them after they've been drawn, we're using mode HGR13? option #2; the 320 by 240 mode.
0 0 2VARIABLE SCREEN1
0 240 2VARIABLE SCREEN2
Now we define the line offsets required by the two pages, so any plotting done will appear on the correct page.
0 VARIABLE WHICHPAGE
: FLIP WHICHPAGE @ IF PAGE2 ELSE PAGE1 THEN
@ VSTART WHICHPAGE 1 TOGGLE ;
This is the flip routine as defined above, so that the ready page will be displayed to the user while the program builds the next one.
: >PAGE WHICHPAGE @ IF SCREEN2 ELSE SCREEN1 THEN
TO VOFFSET ;
And finally in our preparation section, is the routine to point to the invisible page for drawing our figures. Now we can move into the data sections, which begin with the formulas needed to project a point on the screen. Doing a three dimensional projection is not a difficult operation, though it is mentally confusing as you apply the different angles to the viewing coordinates. We also need to keep track of the sines and cosines for each translation that we do, as well as each coordinate to be passed through the formula. So our first helper functions should be a standard method for storing and retrieving these values, such as the two lines below;
: 3@ ( ADR -- X Y Z ) >R R 2@ R> 4+ @ ; -- get values
: 3! ( X Y Z ADR -- ) >R R 4+ ! R> 2! ; -- save values
This simple extension saves having to include a lot of address juggling in our following routines, plus makes our further definitions look cleaner. Next we require the actual data to use in the translation, which we can store as extended double variables;
0. 2VARIABLE ANGLES 0 , -- translation angles
0. 2VARIABLE SINES 0 , -- sines of angle
0. 2VARIABLE COSINES 0 , -- cosines of angles
0. 2VARIABLE 1ST-TRAN 0 , -- camera offset
0. 2VARIABLE 2ND-TRAN 0 , -- projection offset
Using this arrangement the values will be stored in memory in the order of Y, X, Z, because the 2! word reverses the X and Y components when saving to memory. Now we come to the required constants, which define our Z and screen scaling rate;
780 VARAIBLE ZPULL -- how deep z appears
128 CONSTANT MAGIC -- screen scaling.
Now we get to the first slice of the routine, setting up the variables for the translation matrix. All this routine needs to do is get the sines and cosines of the angles and update the values so we'll have them ready for us in the main function;
: SNS ANGLES 3@ SIN ROT SIN ROT SIN ROT SINES 3! -- do sines
ANGLES 3@ COS ROT COS ROT COS ROT COSINES 3! ; -- and cosines
Now comes the routine that will make you mean to your dog, wife, children or co-workers; namely the major translation of points on the screen. This is a very busy and math intensive process, as we convert each point using the angles into an on-screen coordinate;
: | ( N ADR -- N' ) @ 10000 */ ; -- a helper, get decimal and compute integer value.
: PROJECT ( X Y Z - X' Y' ) ( PROJECT POINT X Y Z ON SCREEN )
1ST-TRAN 3@ -- get values, stack: x y z x y z
4 ROLL + >R -- add z's and move to return stack
ROT + >R -- stack: x y x y add y's, move to return stack
+ DUP -- add x's, make copy. stack: x x
COSINES 4+ | -- multiply by cos(z) stack: x x*cos(z)
R SINES 4+ | - -- subtract y*sin(z) stack: x x*cos(z)-y*sin(Z)
SWAP SINES 4+ | -- multiply by sin(z) stack: x*cos(z)-y*sin(z) x*sin(z)
R> COSINES 4+ | + -- add y*cos(z) stack: x*cos(z)-y*sin(z) x*sin(z)+y*cos(z)
R> DUP -- get z and make copy stack: x1 y1 z z
COSINES | -- multiply by cos(y) stack: x1 y1 z z*cos(y)
4 PICK SINES | -- multiply x1 by sin(y) = (x*cos(z)-y*sin(z))*sin(y)
- SWAP -- save z1, get z. stack: x1 y1 z1 z
SINES | -- multiply z by sin(y)
4 PICK COSINES | -- get x1 and multiply by cos(y)
+ 3 PUT -- add to z*sin(y)=z*sin(y)+ (x*cos(z)-y*sin(z)), store in x1
OVER COSINES 2+ | -- multiply y1 by cos(x) stack: x2 y1 z1 y1*cos(x)
OVER SINES 2+ | -- multiply z1 by sin(x) stack: x2 y1 z1 y1*cos(x) z1*sin(x)
- ROT SINES 2+ | -- subtract, save as z2 & times sin(x) stack: x2 z1 z2 y1*sin(x)
ROT COSINES 2+ | + -- add z1*cos(x). stack: x2 y2 z2
2ND-TRAN 3@ -- get 2nd translation. stack: x2 y2 z2 x y z
4 ROLL + -ROT -- add z's together
4 ROLL + -ROT -- add y's together
4 ROLL + -ROT -- add x's too
ZPULL @ - MINUS >R -- subtract z pull from z2 and save on return stack
MAGIC R */ MAXY 2 / + -- compute point versus screen center
SWAP MAGIC R> */ MAXX 2 / + -- do same for x
SWAP ; -- and return X and Y
I did say it was a complex process, so if you weren't able to follow it all that's okay too. Doing such a projection is beyond the scope of this manual, so get a good math book if you need one.
With the major routine now complete we can move on to the drawing routines, which supply the project process with the data points to be converted for plotting. To make this the most useful we're going to implement a Turtle Graphics system, which contains a data buffer in the available RAM;
0 VARIABLE DATA -- address of data area
0 VARIABLE EDATA -- end pointer of buffer
These variables will set up our controls for the data buffer, including an end of array value we can test for running out of memory. Now we'll need our functions;
: DATA> ( -- N ) DATA @ @ 2 DATA +! ; -- fetch data and advance pointer
: >DATA ( N -- ) DATA @ ! 2 DATA +! ; -- save value, advance pointer.
These routines will fetch or store a value from the buffer in a first in-first out manner, allowing the turtle routines to stream the data through itself over and over. Note that these routines do not test versus the data end variable EDATA, this is up to the point creation routines to do.
Our next double process is that of getting and saving the data points themselves, for which we borrow the coordinate fetch and store routines defined earlier;
: XYZ> ( -- X Y Z ) DATA @ 3@ 6 DATA +! ; -- get x y and z
: >XYZ ( X Y Z -- ) DATA @ 3! 6 DATA +! ; -- save x y and z
Now we can create the actual turtle code, which will use DATA> to fetch commands and XYZ> to fetch coordinates. Like most turtles however, it must have an idea of where it's starting from for each command, so needs a bit of memory.
0 VARIABLE LAST-X -- where was I?
0 VARIABLE LAST-Y -- where am I?
0 VARIABLE LAST-C -- what color am me?
Note that these values are only 2D coordinates, because the turtle only keeps track of on-screen entries. Now we can make our turtle functions;
: MOVE-TO XYZ> PROJECT LAST-Y ! LAST-X ! ; -- just move
: DRAW-TO XYZ> PROJECT 2DUP LAST-X @ LAST-Y@ -- move while drawing
LAST-C @ LINE LAST-Y ! LAST-X ! ; -- update place
: POINT-AT XYZ> PROJECT 2DUP LAST-C @ PLOT -- just a point
LAST-Y ! LAST-X ! ; -- update place
: CMD? ( -- N ) DATA> DUP LAST-C ! 256 / ; - get cmd and set color.
Now we're ready for our turtle;
: TURTLE ( -- ) PAD DATA ! -- start at data top
BEGIN CMD? DUP WHILE -- while we find commands
DUP 1 = IF POINT-AT THEN -- test point cmd #1
DUP 2 = IF MOVE-TO THEN -- test move cmd #2
3 = IF DRAW-TO THEN -- test draw cmd #3
REPEAT DROP ; -- and loop it
And finally, because this is a double buffered process that has one invisible page and one display page, we need to both flip the pages and make a clean copy of the original background on the new drawing frame. For this program, we'll simply wipe the invisible page to black;
: QCLS WHICHPAGE @ IF PAGE1 -- select which page offset
ELSE PAGE2 THEN @ -- then get the value
&F02 &3C4 P! -- enable all planes in the card
0 OVER &A000 L! -- write a zero to all at once
DUP 1+ PAGE-SIZE WR1 ; -- then call the wipe function
: DO-ONE QCLS -- erase page
TURTLE -- draw figures
( VSYNC ) -- optional retrace to slow it
FLIP >PAGE ; -- and flip!
Now let's create a demo routine to actually build some data, rotate the mess in three dimensions, and to show that our turtle knows what its doing. For this purpose of course, we're once again going to need a random number generator like this code;
: RND ( N -- XN ) RND0 ABS SWAP MOD ; -- return 0 to n-1 inclusive
Having done this, we're ready to make some 3 dimensional stars, using the following constants to control it;
800 CONSTANT MAXSTARS -- how many stars to make (more or less)
300 CONSTANT MAXLOOPS -- how many loops to go through.
: MAKESTARS MAXSTARS RND 5 MAX -- select a random number of stars. (5 min.)
0 DO 800 RND 400 - -- start loop, select X location +-400 units
800 RND 400 - -- make a Y location
800 RND 400 - -- make a Z location
271 >DATA >XYZ -- give turtle the command and data
LOOP 0 >DATA DATA -> EDATA ; -- loop until done, then save end of buffer
This routine will make the stars in the data buffer, along with the command word the turtle needs. In this case the command word is 271, or 256 plus 15. Since 256 is the command byte this calls the "point at" command of the turtle, using the color of 15 or bright white on the standard palette.
Now it's time to adjust the angles as we roll about the field of stars, or the field moves in front of us. (It's really hard to tell which at times.) The first routine will advance a value and then reset it when it hits 360 degrees, which is used by the angle advance process;
: +360 ( n x y z -- x y z+n ) 4 ROLL + 360 MOD -ROT ;
And now we do the same for all three angles, given the amount to advance;
: +ANG ( X Y Z -- ) ANGLES 3@ +360 +360 +360 ANGLES 3! SNS ;
This routine also calls the SNS function, which gets the new sines and cosines required for the new perspective. The last routine is the one that pulls this all together, draws the picture and keeps going until told to stop or the max-loops value is reached;
: STAR-LOOP MAXLOOPS 0 DO -- start the loop
DO-ONE 1 2 4 +ANG -- do-it once and advance angle
?TERMINAL IF LEAVE THEN -- see if user wants to stop
LOOP ; -- then loop.
Now, because we used Turtle Graphics we can also make the display do a bunch of lines, with a build routine that looks like this;
: MAKELINES MAXSTARS 2 / RND 6 MAX -- select a random number of lines. (3 min.)
0 DO 800 RND 400 - -- start loop, select X location +-400 units
800 RND 400 - -- make a Y location
800 RND 400 - -- make a Z location
527 >DATA >XYZ -- move turtle to start
800 RND 400 - -- make a X location
800 RND 400 - -- make a Y location
800 RND 400 - -- make a Z location
783 >DATA >XYZ -- tell turtle to draw to the end
LOOP 0 >DATA DATA -> EDATA ; -- loop until done, then save end of buffer
Even using this routine the same STAR-LOOP above can be employed to rotate and display the result, for only the data has been changed and the turtle can interpret the commands given it. See the enclosed file 3D.4TH for an example of using the turtle to describe cubes within the 3 Dimensional space, you might even be inclined to create your own world display in the thing.
SEE ALSO: File 3D1.4TH contains the 3D projection system listed above as modified to use the HGR modes of operation. Programmers should note the change in function of VSTART and the missing QCLS and page sizing operations. Step lightly.
For a second program that calls for user input and uses bit-map images, we're going to build a copy of that trusty and rusty game called Break-Out. Because this game is not graphically intensive we're going to build it in HGR mode, which has a higher pixel count so our images can be more complex. For our interface we'll simply be using the mouse as the paddle position indicator, without clicks or buttons or other action required. The game will operate in one of three modes; Demonstration, 1 Player and 2 Player just for convenience.
The reason I've selected Break-Out is because it was one of the first video game programs I ever wrote, plus an ex-roommate loved the game which is why I bothered in the first place. So, in order to make this a potential executable we have to define the vocabulary for the game itself, a simple matter of the following words;
ONLY VOCABULARY BREAK-GAME IMMEDIATE
VIDEO ALSO FORTH ALSO
BREAK-GAME DEFINITIONS
This sets up our vocabulary as a function of the ROOT search process, just like all other vocabularies in the system. Since we expect to be calling the video often we've included it in the search path, to save typing in a bunch of overrides.
Now we jump right into the meat of the application as we must define the first portion of our graphics, namely that of the ball which will be used to remove bricks from the screen;
11 7 7 * 9 * + ARRAY BALL
This forms the basis of our ball data, a small array meant to hold several values and the pictures themselves. Note that the first value is 11 when I really need 10 bytes, because I always like giving Forth an extra value just to preserve the dictionary chain. The data being used is X position, Y position, X Speed, Y Speed and Picture Number, followed by 9 images of 7 pixels by 7 lines.
Now we define the dotted shortcuts for our use in the event we need them, even though we may not require them all during the course of writing the program. (But they are useful for testing too.) Note that we are only giving the widths from the current location in the array for these values, allowing the input processor to manage our pointer offsets.
0 +OFF .X -- x position*10
2 +OFF .Y -- y position*10
2 +OFF .XS -- x speed
2 +OFF .YS -- y speed
Oddly enough, these values will work nicely, creating the proper addition functions to return our addresses. Entering BALL.XS for example will return the address of the array and add 4 to it to point to the X speed value, or .XS will add 4 to any value on the stack. Fig-Forth's input processor makes this operation completely transparent to us, so we don't have to bother with creating functions to do this.
Now, to make the game more interesting I decided to build multiple images of the ball, so we'll need a value to tell the program what was the last image used;
2 +OFF .IM -- image # times 100
This concludes the actual hard data contained in the ball structure, the first 10 bytes of the array used up above. The other 441 bytes is used to save ball pictures after we have built them, so we start to define our basic ball functions;
: .PIC ( ADR -- ADR ) .IM DUP @ 100 / 49 * + 2+ ;
This routine will retrieve the value saved in BALL.IM and then divide by 100, multiplying the result by the size of each ball image. (7 pixels by 7 lines.) After finding this value it adds the BALL.IM address and advances by two bytes, to point to the proper picture when we call it.
: .PIC+ ( ADR -- ) .IM DUP @ 1+ 800 MOD SWAP ! ;
This function will increment the BALL.IM value and roll it around when we cover 8 images, so we don't have to worry about the picture get above pointing to a bad address.
: .BALL BALL.PIC 7 7 BALL 2@ 10 / SWAP 10 / 4 VMODE C!
PUTPIC 0 VMODE C! BALL.PIC+ ;
This is our main routine for the ball function, that of showing the ball upon the video screen. For this purpose the values of BALL.X and BALL.Y are multiplied by 10 to increase ball accuracy, such that the ball can move in smaller increments in all directions. Note that after being called 100 times the function BALL.PIC will change to the next image, giving our ball the capacity to carry "texture" or "spin" along with its image. Now we need the last ball function, removing it;
: ~BALL BALL.IM 394 + 7 7 BALL 2@ 10 / SWAP 10 / 4 VMODE C!
PUTPIC 0 VMODE C! ;
This routine will use the 9th bit-map in the BALL array to remove the ball from the user's screen, so we can advance its position and paint it again. Of particular importance is the use of VMODE in these last routines, which by setting the position VMODE to 4 we insure that zero pixels are not drawn.
Let me restate that. An alternate method of removing the ball from the screen could be the routine listed below;
: ~BALL BALL 2@ 10 / SWAP 10 / 7 7 0 SQUARE ; -- hide it
However, this routine has the problem of producing a complete square of black when we want our ball to be round. While this will remove the ball it can also "clip" the corners of bricks on the screen when the ball fails to strike them, and since we don't re-draw the bricks all the time this can be annoying. Besides, if the square does clip the edge of a brick the player will be screaming, "I hit that one! Why didn't it go away?"
So we use the method of the Mask Mode of the PUTPIC function, in that any color defined as pixel zero does not get written to the screen. This means our ball and its blank image are actually round, and no corners of bricks get corrupted.
Now we face the task of actually building our ball graphic, which can be inconvenient using raw data. So for a bit of a shortcut we're going to ask the compiler to create the images, then we'll just "capture" them right off the screen;
: BALL-BLANK ( -- ) 3 3 3 255 -1 CIRCLE BALL.IM 394 + 7 7 0. GETPIC ;
This routine will produce our blank ball by using color 255 for a circle, then will grab the image into the correct place in the array. Doing the ball itself however causes a bit of a problem, for we need to add our "patterning" to the image created;
: GRAB1 2DUP 0 PLOT
BALL.PIC 7 7 0. GETPIC
100 BALL.IM +! 15 PLOT ;
This routine consists of three parts to create our ball graphic. First, it plots a specified location with color 0 and then grabs the images, finally plotting the location specified with color 15. (White.) Last, it adds 100 to the image counter, to make sure the BALL.PIC routine returns the next address. The reason we did the plotting stuff will become obvious after the next function;
: BUILD-BALL ( -- ) 3 3 3 15 -1 CIRCLE 2 1 GRAB1 4 1 GRAB1
5 2 GRAB1 5 4 GRAB1 4 5 GRAB1 2 5 GRAB1 1 4 GRAB1
3 3 GRAB1 0 BALL.IM ! BALL-BLANK ;
This is our ball graphic build function, which first draws a circle in the top left corner of the screen. Then it selectively resets certain pixels as it grabs the images for the ball, then finally calls the blank builder to erase the ball on the screen and save the last picture. The pixels being reset look like the iris of an eyeball as the ball moves on the screen, looking upper-left, up, upper-right, right and so on. The final image has the darker spot located exactly in the center of the ball, as though the eye is looking at the player. While this is not the best way to texture the ball it is simple and easy, so this will do for now.
Now we come to the bricks themselves which are constructed in much the same manner, though in this case we don't need to capture them. For the time being we're going to use bricks that have no texture or lighting, being simply a square of the appropriate color. For our size we'll use 20 bricks across the line by 16 lines of them, for a total of 320 bricks. We'll have 8 colors, so we need 8 graphics;
28 14 * 8 * 1+ ARRAY BRICKS
Again, we add 1 extra byte just to preserve the dictionary, and now we move on to the brick builder;
: >BRICK ( N -- ADR ) 392 * BRICKS + ;
This is our first routine, that of pointing to any brick image. As with the ball we multiply by the size of the images and then add the base address, and we're ready for the build routine;
: MAKE-BRICK ( C A -- ) >BRICK 14 0 DO 28 0 DO 2DUP C! 1+ LOOP
LOOP 2DROP ;
This routine will make a brick in memory given its color and image number, using the two values and not a lot else. Now we'll build them all;
: MAKE-EM 15 9 DO I DUP 9 - MAKE-BRICK LOOP 43 6 MAKE-BRICK
88 7 MAKE-BRICK ;
Now this routine is a little different, for it uses a do loop to create the first five images. We can do this because we intend to use the major colors of the standard palette for the first five bricks, namely bright blue, bright green, bright red, etc. The last two bricks however must call upon colors outside the range of the first 16 available, so we deal with them separately.
Next we come to the last of the graphics required for the game, that of the borders and window of the play area. For this purpose we're not going to bother to store the image at all, but simply draw it and forget it;
: DRAW-WALLS 15 10 DO 0 I 589 I 15 LINE LOOP
5 0 DO I 10 I MAXY 15 LINE 586 I + 10 OVER MAXY 15 LINE LOOP
5 15 580 460 0 SQUARE ;
The first loop here draws the top edge just a few lines from the screen top, while the second loop draws the left and right border. The last command is used to clear the play area though is not really necessary, unless of course you're testing on the screen and have filled the area with characters. Because our game also does on-screen pixel detection it's a good idea to make sure the slate is clean, so this routine will do that while creating the wall.
Now we come to defining what goes into that play area given that we have all of the graphics, and retaining that data as we switch from screen to screen. "Switch from screen to screen?" I hear you say, "What are you talking about?"
Remember, we're building a multi-player game so we need twice as much data, one for player 1 and one for player 2. Since we have two players, 8 types of bricks displayed in 16 lines, and 20 bricks across a line this could be our array definition;
20 8 * 2 * 2 * ARRAY PIPS -- the bricks laid out
The term "pip" in this case is not unlike the dots on a card or a pair of dice, or in fact the letters upon the standard text screen. Like the letter images our brick array up above is the picture of the data as it appears on the screen, while the values stored in the pips array are the ASCII meanings of those characters. For those into game programming this is known as "tiling," or the practice of having a collection of fixed images that are intermixed upon the screen to create a complete image. This is a highly useful practice when dealing with graphics, for without it we must maintain a complete copy of every pixel we place into the video area. (And deal with the delay caused from moving all those pixels around.)
Now we need to know which player is "up," a simple matter of a variable;
0 VARIABLE PLAYER
And we define a routine to fill the array with the bricks in the manner we desire, namely 16 rows of 20 bricks in a line for a player. In this case however our bricks above are numbered as types 0 to 7, while we'd probably want to use 0 as indicating no brick. No problem, we just offset the values by 1;
: BUILD-BRICKS 8 16 0 DO 20 0 DO DUP I J 20 * + PIPS + 320
PLAYER @ * + C! LOOP I 1 AND - LOOP DROP ;
This routine will build the bricks into the PIPS array for us, creating the layout we'll need at the start of the game. Note that this routine uses the player number to offset to the number of bricks, by taking the player value and multiplying it by 320. This conceivably could indicate even more players could be allowed, but for now we're sticking to two.
Next we get to our print bricks function that will be called at the start of each round, placing the bricks in memory into their proper screen locations. Note that though our bricks are 28 pixels wide by 14 lines high we actually display them on a 29 pixel by 15 line grid, to give each block a black edging;
: .BRICKS 320 0 DO PLAYER @ 320 * PIPS + I + C@ -DUP IF 1-
>BRICK 28 14 I 20 /MOD 15 * 50 + SWAP 29 * 6 + SWAP PUTPIC
THEN LOOP ;
This will print our bricks as contained in the currently active array, offset by the values of 6 X and 50 Y for proper centering. Our next routine is to find out if the player has removed all the bricks, which indicates that we need to build a new set;
: ALL-GONE? ( -- F ) 0 320 0 DO PLAYER @ 320 * PIPS + I + C@ OR LOOP 0= ;
This routine takes advantage of the zero being stored in the pips array, because it simply performs a logical OR of all the bricks together to make up a number. If any are left the result won't be zero so the flag will come back as being false, so we don't have to bother with checking brick type.
: ALL-MAKE HGR CLS BUILD-BALL MAKE-EM 0 PLAYER ! BUILD-BRICKS
1 PLAYER ! BUILD-BRICKS 0 PLAYER ! ;
This forms the basis of our game start-up routine in that everything is constructed all at once, so that we don't have to do it all later. At this point we can call the routines and see our result, as in;
ALL-MAKE DRAW-WALLS .BRICKS .BALL
The ball of course will "lay over" the top edge of the game window because it's located at 0, 0 but at least this gives us an impression of how the game will look.
Now we come to the point of making detections and controlling the ball, which can drive you crazy if you aren't careful. First, we'll include those routines to reverse the ball's direction assuming it has hit something, namely a left to right or up and down reversal of the speed values;
: INV-X BALL.XS @ MINUS BALL.XS ! ; -- invert x direction
: INV-Y BALL.YS @ MINUS BALL.YS ! ; -- invert y direction
This is nice because we used integers which are easy to manipulate even though we know the speed and position values are multiplied by 10. Now we come to the color detector;
Though Fig-Forth contains routines designed to do pixel detection they are not well geared for this game's purpose, in that they require a color to be detected against;
50 50 10 10 5 ?BOX -- test a 10 by 10 box at 50, 50 for color 5
In Break-Out we have up to 8 different colors that need to be detected, not including the white which would be the game paddle. (The walls we assume would never be touched, since the ball movement routine will clip them automatically.) In addition, the above routine tests 40 pixels around the edge and doesn't give us an indication of where it found the color, so is not exactly useful to us in this instance. (The detect routines in Fig-Forth are meant for "bounding boxes" in graphics parlance.)
So, we need a routine that will return a true when it encounters the pixel of a brick;
: ?BRIK ?PLOT 9 250 WITHIN ; -- color detector
This routine is like ?PLOT which returns a pixel's color, except that it confirms that the color is in the range of the bricks and a few more. (Okay, a lot more.) Now we need to set up a detection scheme to find out if the ball hit anything, using this color detector to indicate a collision.
For our purposes we will be using the famous 8-point method common to symmetrical objects, where 8 pixels around the object will themselves be tested. (See diagram.) This corresponds to a top-left, top-right, left-upper, right-upper, and so on method, with only the first detection being counted as valid;
0 VARIABLE DET 0 VARIABLE X1 0 VARIABLE Y1
The variable DET is our indicator that a collision has taken place and we should stop testing, while the value of X1 and Y1 are the offset relative to the ball where the event took place. The reason we do this is to create an "internal" image of where the ball is when it actually strikes an object, such that we can use this false image to decide which brick was hit. Now comes our detection lines;
: D1 DET @ IF OVER 1+ OVER 1- ?BRIK IF 0 DET ! 10 X1 ! -10
Y1 ! INV-Y THEN THEN ; -- top-left
: D2 DET @ IF OVER 5 + OVER 1- ?BRIK IF 0 DET ! 50 X1 ! -10
Y1 ! INV-Y THEN THEN ; -- top-right
And so on. Each of these words expect the X and Y position to be located on the stack, selecting the pixel they desire and making the test until DET becomes false. By using DET as true we can easily grab the value to enable all of the tests, then short-circuit the routines when a collision has been sensed. Finally, this brings up the master detection loop;
: SLAM? ( -- ) 1 DET ! BALL 2@ 10 / SWAP 10 / D1 D2 D3 D4 D5
D6 D7 D8 2DROP ; -- did we hit something?
This runs through our tests one at a time after fetching the ball location, adjusting it to screen pixels and then testing the hits. Now we need a routine to remove bricks once they have been hit, which also shows the advantages of the X1 and Y1 variables;
: SND1 SOUND SOUND? 1 AND IF -PLAY ," BOING.WAV" <PLAY>
THEN BREAK-GAME ; -- play sound if brick hit
: ~BRICK ( -- N ) BALL 2@ 60 - X1 @ + 290 / -- compute x
SWAP 500 - Y1 @ + 150 / -- compute y
20 * + DUP -- compute brick #
20 /MOD 15 * 50 + SWAP 29 * 6 + SWAP 28 14 0 SQUARE -- remove from screen
PIPS + PLAYER @ 320 * + DUP C@ 0 ROT C! SND1 ; -- remove from array
The first line in this routine computes the location of the ball given that SLAM? has reported which pixel detected a brick, by adjusting the ball's X location to that point. The next line does the same for the Y coordinate of the ball, as each are divided by the display width of the bricks times 10 in pixels. Finally, having computed the brick location in both X and Y the combined offset is found, which is used to remove the block from the screen and within the memory array. In the array portion the routine returns which brick type was hit, to be used in scoring the game later. And last of all the routine calls on the sound card to play a sound when the brick is hit, which makes the game more entertaining.
Now we enter the major routine of moving the ball about on the screen, but since we've constructed the basics it becomes a simple matter of advancing the variables and making a few tests;
: ADV-BAL ( -- ) ~BALL BALL 2@ BALL.XS 2@ ROT + -ROT + SWAP
DUP 60 < IF DROP 60 INV-X SND3 THEN
DUP 5780 > IF DROP 5780 INV-X SND3 THEN SWAP
DUP 160 < IF DROP 160 INV-Y SND2 THEN
( DUP 4710 > IF DROP 4710 INV-Y SND3 THEN )
SWAP BALL 2! SLAM? .BALL ;
This routine adds the speed values to the current position after blanking the ball, then tests for the ball hitting the left wall, the right wall, and the top wall. Finally the new ball position is saved in the array and the detection system is called, then the ball is displayed on the screen once more. Like the brick remove function each encounter carries with it another sound, again to make the game interesting. The parenthesized line is used for testing in that it creates another wall located at the screen bottom, which is handy to see that everything works;
: DRIFT BEGIN ADV-BAL 5000 0 DO NOOP LOOP ?TERMINAL UNTIL ;
This routine will let the ball move about until we tell it to stop, which can be used to play with various speed values and see how the ball moves. Now we print everything;
: .ALL CLS DRAW-WALLS .BRICKS ;
Easy enough, but there's still more to do...
Our next task is that of setting up the scoring system and its report, which because we are using the HGR mode calls the Font Printer and two selected internal fonts;
: FNT1 1 FY* ! 1 FX* ! -3 FONT HOME ;
: FNT2 2 FX* ! 3 FY* ! -1 FONT HOME ;
These two lines will set up each font and home the cursor using it's values, because the multiplication factor of each font is different. Font 1 is the smallest font currently contained inside the Fig-Forth kernel space, a proportional tightly packed single-dot character set that contains minimal white space. The second font is a double-dot face almost identical to the standard text screen, with fixed pitch spacing and very clear characters. For our purposes the second font is scaled up in size, appearing twice as wide and three times its height in this program.
0. 2VARIABLE SCORE1 0. 2VARIABLE SCORE2
Next come the score variables required of the game, two double word numbers that are more than large enough to handle any likely value. Now we come to the score routines, the first being to return the address of the effective score for the current player;
: SCORE ( -- ADR ) PLAYER @ IF SCORE2 ELSE SCORE1 THEN ;
As in other areas of the game this uses the value contained in the PLAYER variable to know which address should be returned and can be expanded as shown below for more than two players;
5 4 * ARRAY SCORES
: SCORE ( -- ADR ) PLAYER @ 4 * SCORES + ;
Next we come to the score printer, which uses the number formatting routines of Fig-Forth. In this case however, we also make a deliberate change to the value being printed because the game will never increment the scores by 1, so we force the first digit to zero and print the remaining values as though they were multiplied by 10;
: .SCORE SCORE 2@ <# 48 HOLD # # BEGIN 2DUP OR WHILE 44 HOLD
# # # REPEAT #> FNT1 TYPE ; ( PRINT #*10 W/ COMMAS )
The above routine also inserts a comma into the score display for every third digit, making the value of 1000 appear as 10,000. It does this by first forcing the zero in the one's place and extracting the first two digits of the actual score variable, then testing to see if any more digits remain in the number. If the number still has digits it adds a comma and extracts the next three digits, to repeat the process until the number is consumed. Finally, font definition 1 is selected and the result is printed, located at the top of the playing field just above the top wall.
: +SCORE ( N -- ) 0 SCORE 2+! .SCORE ;
This next routine adds to the score the value of the last brick removed, or 1 to 8 from our graphic and "pip" processes above. Because the score is a double number we place a zero on top of the brick number to make it a positive signed value, then add to the player's score for display by .SCORE. As mentioned before, .SCORE prints the values as being ten times the contents of the variable, so no block appears to be worth less than 10 points.
Now we enter the area of printing the other game stats, namely which ball is in play and which player is up. For this of course we need variables to hold the ball counts, plus a constant to know how many balls are allowed in a game;
5 CONSTANT BALLS 5 VARIABLE BALL1 5 VARIABLE BALL2
As before, we need to return the ball address for printing or other use, so have a second definition like that of SCORE;
: BALLC PLAYER @ IF BALL2 ELSE BALL1 THEN ;
Now we can do the print itself;
: .BALLC FNT1 100 600 CUR 2! ." BALL" FNT2 40 305 CUR 2! BALLC
@ 48 + EMIT FNT1 ;
This routine selects the font desired and locates the cursor for both the title and the value, then returns to the default font of number 1 for printing the score. (And for allowing us to have the smaller font should we interrupt the game.) Now we do the same process for which player is up;
: .PLAYR FNT1 400 600 CUR 2! ." PLAYER" FNT2 140 305 CUR 2!
PLAYER @ 49 + EMIT ;
Both the current ball printer and the current player printer use the ASCII character set to display their values, by adding the character number of "0" or "1" on the ASCII table to present their data. This means that these routines have a maximum display value of 9 before an error, when we have to call upon the number routines of Forth once again.
: UP-SCR .SCORE .BALLC .PLAYR FNT1 ;
Finally, we use this routine to update the whole of the information display, calling each of the last functions as an entire group.
Now comes the main purpose of the program; playing the game. As stated when we started out this version of the program would use the mouse as it's primary input device, mostly because the mouse is easy to both use and program without time based routines. (The typical joystick is a time based device that requires a trigger, a delay and then a read function.) So, as with everything else, our first function is a variable to define the paddle location;
0 VARIABLE PADDLE -- the paddle selector
0 VARIABLE P1 -- and a temporary copy of its position
5 CONSTANT PDH -- the paddle height
Next, in order to make the game successively more difficult we shorten the paddle width as the game progresses, requiring additional constants to go along with the paddle height up above;
40 CONSTANT PD1 -- easy paddle width
30 CONSTANT PD2 -- tougher paddle width
20 CONSTANT PD3 -- hard paddle width
Just as we did with the scores, brick field, and other data areas, we create a routine to return the width of the paddle according to the current status of the game;
: PDS ( -- N ) PADDLE @ DUP 0 = IF DROP PD1 ELSE 1 = IF PD2
ELSE PD3 THEN THEN ; ( RETURN CORRECT SIZE )
Now that our data is complete we can add the drawing routines, the first being the hide function or to remove the old paddle image.
: ~PAD ( -- ) P1 @ 6 MAX 583 PDS - MIN 470 PDS PDH 0 SQUARE ;
( HIDE PADDLE )
This routine uses the value saved in P1 to decide where to draw the black square, limiting its range to the playing field. Now comes the opposite function, draw the paddle;
: .PAD ( X -- ) DUP P1 @ <> IF ~PAD DUP P1 ! 6 MAX 583 PDS -
MIN 470 PDS PDH 15 SQUARE ELSE DROP THEN ;
( SHOW PADDLE )
Because this routine is dot-paddle (print paddle) it requires a parameter telling it where the mouse is currently located, which it compares to the last image of the paddle drawn on the screen. If the two are not equal the old image is erased and a new one is created, which prevents the paddle from flickering from multiple tests.
Now we check to see if the ball has hit the paddle at all, adding a bit of "English" to the ball's movement depending on where the ball struck the paddle surface. (I won't explain exactly how, see if you can figure it out.)
: HPAD? ( -- ) DET @ 0= BALL.Y @ 4600 > AND IF SND3 2 DET !
BALL.YS @ ABS MINUS BALL.YS ! BALL @ 10 / P1 @ - 20 MOD 10
- 3 / BALL.XS @ + DUP -6 < IF DROP -6 THEN DUP 6 > IF DROP
6 THEN BALL.XS ! THEN ; ( HAVE WE HIT THE PADDLE? )
For the final aspect of the paddle we need to retrieve its position as read by the mouse, or from the ball directly if we are in demo mode. Again, we need a variable to indicate we are in demo mode or if we read the mouse, then the routine which uses it;
1 VARIABLE DEM -- demo mode?
: PX ( -- X ) DEM @ IF BALL @ 10 / ELSE MOUSE MBUTTON DROP MX 2
* BREAK-GAME THEN PDS 2 / - 6 MAX 583 PDS - MIN ;
This routine will extract the X value of the ball's position if the demo flag is true, or will read the mouse through MBUTTON and then return the X value of the mouse for the game to use.
Our next task is to create a variable starting position for the ball to be served to the user, as selected at the time the round begins;
: GET-TIME ( -- DX ) 0. 0 &2C00 &21 INT# 4DROP ; -- get time
Here's our time routine again to return the hour, minute and seconds value from the system, to use them for selecting the position of the ball. In this case, only the seconds and milliseconds value is used however, to select the position as shown below;
: SERV GET-TIME 400 MOD 200 - 320 + 10 * BALL ! 3000 BALL.Y ! ;
This routine will do nicely for our ball starting point, and now we create our wait routine. For our game this delay will be fixed for the purposes of the demo mode, but will only start after the user moves the paddle for the player modes. This is achieved by the code shown;
: WAITER DEM @ 0= IF PX BEGIN PX OVER <> UNTIL -- wait for movement
DROP THEN 150 0 DO VSYNC PX .PAD LOOP ; -- then delay
And lastly in the timing section, we need a general delay routine to control game speed;
: DELAY1 90 0 DO HSYNC LOOP ;
This particular delay calls upon another timing signal of the video display screen; that of the Horizontal Retrace time period. Like the vertical retrace this signal occurs all of the time the display is active, though has a higher frequency than the VSYNC. The vertical signal is too slow in this case to provide an accurate time base, so we have opted to use the horizontal to synchronize our game.
Now we come down to the final routines of the game to tie it all together, first starting with the last type of detection required by the program. This routine is quite simple in that it detects if the ball hits something, then updates the score, paddle, display, a count of successful hits and detects the speed blocks to increase the ball's movement and difficulty of the game;
0 VARIABLE GOOD -- how many bricks have been hit
: DET1 DET @ 0= IF -- brick hit?
~BRICK DUP 1 GOOD +! -- remove it and count
PADDLE @ + +SCORE -- add score
UP-SCR 3 > BALL.XS 2@ ABS -- update screen, change ball speed
SWAP ABS MAX 8 < AND IF -- if ball hits higher block.
BALL.XS DUP 2@ 14 10 */ -- phase 1 speed adjust
SWAP 14 10 */ SWAP ROT 2! -- phase 2 speed adjust
THEN THEN ; -- exit.
Next comes the result of hitting multiple blocks, namely the shortening paddle defined in those routines. For our game we're going to assume the game is too easy for the player if they manage to remove 100 bricks in a row, thus requiring a smaller paddle to make the game more challenging;
: GOOD1 DET @ 0= IF GOOD @ 100 > IF
~PAD PADDLE @ 1+ 2 MIN 0 P1
! PADDLE ! PX .PAD 0 GOOD ! THEN THEN ;
And now we come to our first main loop, the routine that brings all of the functions together to run a single round. This too will become a polled operation, moving the ball, doing all the detection required, managing the paddle and delays, even building more bricks if the user clears the field;
: MAIN1 ADV-BAL PX .PAD HPAD? DELAY1
DET1 GOOD1 ALL-GONE? DET @ 2 = AND
IF BUILD-BRICKS .BRICKS THEN ;
Now we come to the switch player orphan routine that draws everything at once, to produce the starting display at the start of each round;
: .ALL DRAW-WALLS .BRICKS UP-SCR ; ( DRAW BASE SCREEN )
And finally in this tie-up section, we perform the above processes until the ball has left the screen, or the user taps a key on the console to indicate they want to stop;
: PLAY1 .ALL SERV WAITER BEGIN -- start display and main loop
MAIN1 BALL.Y @ 4720 > ?TERMINAL -- process until key or ball off screen
OR UNTIL ~BALL 1 BALLC -! -- then reset ball speed
3 4 BALL.XS 2! 0 PADDLE ! ; -- and paddle size too.
Now we have to go through our game's initialization and set-up functions, so the screen, mouse and compiler are in the proper modes for operation. First the set-up;
: SET-UP MOUSE MOK? MAXX 0 MSETX -- set up mouse
2DROP BREAK-GAME ALL-MAKE -- set-up game
0. SCORE1 2! 0. SCORE2 2! 0 GOOD ! -- zero variables
0 PADDLE ! 0 DEM ! 0 PLAYER !
4000 3000 BALL 2! 3 4 BALL.XS 2! -- set-up the ball
BALLS DUP BALL1 ! BALL2 ! -- set-up players
HGR -3 FONT >FONT SPACE ; -- and set up the screen
Our next task is that of getting how many players we expect to have in this game by asking the user to give us an answer. Because this game is written from the viewpoint of a stand-alone operation, we do not use the Forth input processor but actually test for the number keys themselves;
0 VARIABLE HOWMANY
: ASK SET-UP -1 FONT 2 FX* ! 2 FY* ! -- set up a new font for this
100 0 CUR 2! ." HOW MANY " -- and ask how many players
." PLAYERS? " BEGIN KEY DUP 47 > -- begin a loop that allows keys
OVER 51 < AND UNTIL 48 - -- 0, 1 and 2 only. (0=demo)
HOWMANY ! -3 FONT HOME SPACE ; -- save value and exit.
And at long last, we reach the end of the program, where we switch back and forth between players and run the whole thing;
: PLAY ASK HOWMANY @ 0= IF 1 DEM ! THEN -- set-up ask and start up
BEGIN PLAY1 HOWMANY @ 2 = IF -- if two players, switch them
PLAYER 1 TOGGLE BALL1 BALL2 OR -- if 2 players, check both out of balls
ELSE BALLC THEN 0= -- else only test player 1
?TERMINAL OR UNTIL -- or if a key is hit, exit
SOUND QUIET MOUSE MCLR -- turn off sound and mouse
BREAK-GAME TEXT ; -- and exit to text mode.
This entire game is in the file BREAK.4TH for your amusement and pleasure, though the listing here in the manual has been changed for greater clarity. Also, the latest version of the Break-Out game runs in full demo mode, contains a high score record and detects for a compatible video mode.
Return to Contents. Next Chapter. Previous Chapter.