How To Program A Game With FreeBASIC - Lesson #1
Written by Lachie D.(January, 2006)
The objective of this series of lessons is to help newbies who know very little of BASIC to learn the basics of programming in FreeBASIC necessary to create any computer game. Some elementary BASIC knowledge would help a lot though I believe that people who don't know BASIC at all should comprehend these lessons too. But applying them might be a problem for those people. I'm using here the word(well it's an acronym) "BASIC" and not FreeBASIC because if you know basics of QuickBASIC, Visual BASIC or any other variant of BASIC these lessons should be very easy to understand.
I'm starting this series because I feel that tutorials of this kind were always something our community was missing. Even before FreeBASIC. I've corresponded during my programming lifetime with few programming newbies and they all had almost identical problems when trying to program a game. So I think I'm able to detect what beginners need quite well and on what way the stuff needs to be explained to them. I also remember my beginnings and the problems I had with using separated routines that were never meant to be combined and used to create a game. The breaking point for me was the moment when I discovered RelLib(a QuickBASIC graphics library by R.E.Lope) and the scrolling engine that was created with it. That scrolling engine motivated me to explore its mechanics and expand on it(with some help from R.E.Lope). In one single moment I acquired the ability to program most of the stuff(necessary to complete a game) by myself. It's like driving a bike. The moment when you really learn it lasts one second.
So that's my goal with this series. To learn you enough so you would be self-sufficient in 90% of cases. And the best way to learn new things is to see them applied. Many tutorials fail in that by being too generic. You will always need help from more expert programmers but the point is that you don't need it on every step. Have in mind that this depends on the type of game you are developing and the graphics library you are using.
The example programs and mini-games we'll create will be coded in GFXlib(a FreeBASIC built-in graphics library). Red Jumpy Ball, ArKade(you might need to look it up in the News Section) and Poxie were coded in it and I think those games are good references. But don't worry. Switching from one graphics library to another is easy when you know how to code in at least one. Also, a really interesting(especially to me) FreeBASIC graphics library named AFLIB2 is currently being developed so I might recode all the examples from this tutorial in it and make them public.
I'm not sure how much time I will have to complete all the lessons so I'll try to make each one standalone and not dependant on the lessons that follow it.
This tutorial will not deal with raycasting engines(3D programming) or something advance like that. If you want that but are a beginner you NEED the following lessons FIRST.
Since we are going to code in FreeBASIC you need to get FreeBASIC first(if you don't have it yet) from http://www.freebasic.net(the examples were compiled with version 0.15b) and FBIDE(the best FreeBASIC graphical user interface at this point) from http://fbide.sourceforge.net.
Example #1: A simple program - The circle moves!
We'll start with some elementary stuff. The first program we'll code will not feature external graphics because loading graphics from external files(usually BMP images) is always a dirty business and will confuse you at this point. Trust me on this. Be patient.
The program we'll create will allow you to move a circle around the screen. A very simple program but through making it we'll learn important facts and a lot of elementary statements and methods necessary to create any game with GFXlib.
As we are using GFXlib you need to be aware of gfxlib.txt file(GFXlib's documentation) placed in the /FreeBASIC/docs directory. That's our Bible and very useful with these lessons since I will not explain every parameter of every statement used in the example programs(most likely).
Open a new program in FBIDE. First thing we'll do is set the graphic mode. What's setting a graphic mode? Choosing the program's graphic resolution and color depth in bits(8bit, 16bit, ...). For example, 8bit color depth is the standard 256 colors mode(8 bits per pixel). The graphic mode is set with the SCREEN statement like this:
13 means 320*200 graphic resolution, 8 means 8bit graphics, 2 means two work pages and 0 means window mode(input 1 for full screen mode). Minimum of 2 work pages is recommended for any graphics dependant program. These things will become clearer a little bit later. For more details about the SCREEN statement refer to GFXlib's documentation(a more "advanced" version of the SCREEN statement is SCREENRES).
The next thing we'll do is set a loop that plays until the user pushes the letter Q on the keyboard. Loops are the foundation of any program, not just a computer game. Coding a program on a way it would stop every now and then and wait for the user to type something in is a BAD and WRONG way to program anything you want for other people to use. We'll use loops as places where the program waits for the user to do something(clicks with mouse or pushes a key) and where the program executes some routine according to user's action. It will also be used as a place where objects not controlled by the player(enemies) are managed/moved. Loops are a must have.
If you are aware of all these things you can skip to the end of this section and download the completed example(with comments). If there is something in it you don't understand then get back here.
We can set a loop on more ways(with WHILE:WEND statements, using the GOTO statement - Noooo!) but the best way is to use DO...LOOP. This type of loop simply repeats a block of statements in it until the condition is met. You set the condition(s) after LOOP with UNTIL. Check the following code:
SCREEN 13,8,2,0 ' Sets the graphic mode DO ' We'll put our statemens here later LOOP UNTIL INKEY$ = "Q" or INKEY$ = "q"
If you compile this code and run it you'll get a small black empty 320*200 window which you can turn off by pushing the letter Q(you might need to hold it). The program simply loops until you press "Q or "q". I used both upper and lower case "Q" symbol in case Caps Lock is turned on on your keyboard. INKEY$ is a statement that returns the last key pushed on the keyboard. I will explain later why it shouldn't be used with games and what's a better substitute.
To draw a circle we'll use the CIRCLE statement(refer to GFXlib's documentation). Check the following code:
SCREEN 13,8,2,0 ' Sets the graphic mode DO CIRCLE (150, 90), 10, 15 LOOP UNTIL INKEY$ = "Q" or INKEY$ = "q"
The last code draws a small circle on coordinates 150, 90 with a radius of 10 and color 15(plain white) in a loop which you can check if you compile the code. So how to move that circle? We need to connect it's coordinates with VARIABLES. For this we'll use two variables named circlex and circley. Check the following code:
SCREEN 13,8,2,0 ' Sets the graphic mode circlex = 150 ' Initial circle position circley = 90 DO CIRCLE (circlex, circlex), 10, 15 LOOP UNTIL INKEY$ = "Q" or INKEY$ = "q"
This makes no change in the result of our program but it's a step to what we want to accomplish. You can change the amounts to which circlex and circley equal to change the circle's initial position but that's not what we really want. In order to move the circle we need to connect circlex and circley variables with keyboard statements.
Now I will introduce a new statement instead of INKEY$ which can detect multiple keypresses and is much more responsive(perfect response) than INKEY$. The flaw of INKEY$, as well as being very non-responsive(which you probably were able to detect when trying to shut down the previously compiled examples), is that it can detect only one keypress at any given moment which renders it completely unusable in games.
The substitute we'll use is MULTIKEY(a GFXlib statement) which features only one parameter and that's the DOS scancode of the key you want to query. You might be lost now. DOS scancode is nothing but a code referred by the computer to a certain keyboard key. If you check Appendix A of the GFXlib's documentation you will see what each code stands for. For example, MULTIKEY(&h1C) queries if you pushed ENTER. To avoid for your code to be hard to understand and read(both to you and others) FreeBASIC allows you to replace these scancodes with "easy to read" constants like it's explained in Appendix A. To avoid the need to copy and paste these constants declarations into your source code you can attach fbgfx.bi module onto your source code which has all these declarations typed in. What's a .bi file? Well, it can be any kind of module you would attach to your source code and which can feature various subroutines(if you don't know what a subroutine is we'll get on that later) and declarations used in your main module. You don't really need to include fbgfx.bi module in this very example since coping and pasting these declarations is an equally right way but you'll have to learn how to include .bi files into your programs eventually. So why not learn it now? The statement you need to add in the code we are working on is just one line as it follows:
It's best to put that line somewhere on the beginning of your program(before or after the sub declarations). You don't need to set a path to fbgfx.bi since it's placed in /FreeBASIC/inc directory. You only need to set a path to a .bi file if it's not in that directory or not in the directory where the source code is.
Now the fun starts.
We will add a new variable named circlespeed which flags(sets) how many pixels the circle will move in one cycle(loop). The movement will be done with arrows key. Every time the user pushes a certain arrow key we will tell the program to change either circlex or circley(depends on the pushed key) by the amount of circlespeed. Check the following code:
#include "fbgfx.bi" SCREEN 13,8,2,0 ' Sets the graphic mode circlex = 150 ' Initial circle position circley = 90 circlespeed = 1 ' Circle's speed => 1 pixel per loop DO CIRCLE (circlex, circley), 10, 15 ' According to pushed key we change the circle's coordinates. IF MULTIKEY(SC_RIGHT) THEN circlex = circlex + circlespeed IF MULTIKEY(SC_LEFT) THEN circlex = circlex - circlespeed IF MULTIKEY(SC_DOWN) THEN circley = circley + circlespeed IF MULTIKEY(SC_UP) THEN circley = circley - circlespeed LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
As you see we also changed the condition after UNTIL since we are using MULTIKEY now. Now you can exit the program by pressing ESCAPE too(I added one more condition).
If you compile the last version of the code two things we don't want to happen will happen. The program will run so fast you won't even notice the movement of the circle and the circle will "smear" the screen(the circles drawn on different coordinates in previous cycles will remain on the screen). To avoid smearing you need to have CLS statement(clears the screen) in the loop so that in every new cycle the old circle from the previous cycle is erased before the new is drawn.
To reduce the speed of the program the quickest fix is the SCREENSYNC statement(place it as the last statement inside the loop(for now)). What it does? It, as it's said in GFXlib's documentation, waits for the screen vertical blank to occur, then returns control to the caller program. Err...this probably confused you and I must admit I don't understand the definition either. What I know is that a similar statement with the same function prevents flickering in QuickBASIC programs. On nowadays PCs it doesn't make much sense since they are very fast. On the other hand it's very useful to FreeBASIC game designers that use GFXlib because it reduces the speed of every program on almost every PC and platform to 85 FPS. It's the best way to ensure(with GFXlib) that your program will run on every PC with almost the same speed. ArKade, Red Jumpy Ball and Poxie used it. If you are some FPS freak and want your game to run with 300+ FPS then you need to find a reliable, millisecond delay routine yourself. I still didn't though some solutions have been suggested to me(later on that).
Copy and paste the following code and compile it:
#include "fbgfx.bi" SCREEN 13,8,2,0 ' Sets the graphic mode circlex = 150 ' Initial circle position circley = 90 circlespeed = 1 ' Circle's speed => 1 pixel per loop DO CLS CIRCLE (circlex, circley), 10, 15 ' According to pushed key we change the circle's coordinates. IF MULTIKEY(SC_RIGHT) THEN circlex = circlex + circlespeed IF MULTIKEY(SC_LEFT) THEN circlex = circlex - circlespeed IF MULTIKEY(SC_DOWN) THEN circley = circley + circlespeed IF MULTIKEY(SC_UP) THEN circley = circley - circlespeed SCREENSYNC LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
Viola! Our circle is moving and "slow enough".
The last version of the code does not represent the desirable way of coding but I had to simplify the code in order to make this lesson easy to understand. What we need to do next is declare the variables on the way they should be declared in any serious program and show why we are having two work pages and what we can do with them.
Now we'll declare the variables used in this example on the "most desirable" way. The first problem with the variables used in the example is that they are not readable in other subroutines and since every serious program is structured with many subroutines this fact is not acceptable. Variables are declared(dimensioned) on this way:
DIM variable_name [AS type_of_variable]
The data inside  is optional and the brackets are not used. Types of variables available in FreeBASIC are BYTE, SHORT, INTEGER, STRING, SINGLE, DOUBLE and few others but I don't find details about them important on this level. What you need to know now is that you should declare variables or arrays AS INTEGER when they hold graphics data(memory buffers holding the graphics) or when they represent data which doesn't need decimal precision(number of lives, points, etc.). Variables that need decimal precision are declared AS SINGLE. Those are usually variables used in games which rely on physics formulae like arcade car driving games or jump 'n run games(gravity effect). Simply, the difference between the speed of two pixels per cycle and the speed of one pixel per cycle is most often too large and in those limits you can't emulate things like fluid movement on the most satisfactory way. Also, behind DIM you should put SHARED which makes that the specific variable readable in the entire program(all subroutines). Don't use SHARED only with variables declared inside subroutines(I do it very rarely). If you are going to declare some variable inside a subroutine I advise you to replace DIM with REDIM(every DIM takes a piece of memory so this prevents some rare but possible cases of unpredictable memory consumption). Strings are used to hold text data.
Like YourName$ = "Dodo". You can omit $ only if you declare YourName AS STRING first.
So we will declare our variables with:
DIM SHARED circlex AS SINGLE DIM SHARED circley AS SINGLE DIM SHARED circlespeed AS SINGLE
You need to place these variable declarations on the beginning of your program after the subroutine declarations. We declared them all AS SINGLE since that gives us more flexibility in speed/movement control(not needed in this very example but most likely will be in your game). They also don't need to be SHARED in this example since we are not using subroutines but you should grow a habit of declaring variables like that.
While this is a perfectly proper way of declaring variables it's not the most convenient way in larger projects where we have huge amount of variables usually associated to several objects(an object can be the player, enemy or anything that is defined with MORE THAN ONE variable).
So first we'll define a user defined data type with the statement TYPE that can contain more variables/arrays(stay with me). We'll name this user data type "ObjectType". The code:
TYPE ObjectType x AS SINGLE y AS SINGLE speed AS SINGLE END TYPE
After this we declare our circle as an object:
DIM SHARED CircleM AS ObjectType ' We can't declare this variable with "Circle" ' since then FB can't differ it from ' the statement CIRCLE thus "CircleM".
How is this method beneficial? It allows us to manage the program variables on a more efficient and cleaner way. Instead of(in this example) having to declare each circle's characteristic(it's position, speed, etc.) separately we'll simply use a type:def that includes all these variables and associate a variable or an array to it(in this case that's CircleM). So now the circle's x position is flagged with CircleM.X, circle's y position with CircleM.Y and circle's speed with CircleM.speed. I hope you see now why this is better. One user defined type can be connected with more variables or arrays. In this example you can add another object with something like 'DIM SHARED EnemyCircle(8) AS ObjectType' which would allow us to manage 8 "evil" circles with a specific set of routines(an AI of some sort) using the variables in the 'ObjectType' type:def(x, y, speed) and these circles could "attack" the user's circle on some way. In the next lesson all this will become more clear. Have in mind that not ALL variables need to be declared using a type:def. This is only for "objects" in your game that are defined(characterized) with more variables(like a hero determined by health, money, score, strength, etc.).
After the change the final version of the code looks like this:
#include "fbgfx.bi" ' Our user defined type. TYPE ObjectType x AS SINGLE y AS SINGLE speed AS SINGLE END TYPE DIM SHARED CircleM AS ObjectType ' We can't declare this variable with "Circle" ' since then FB can't differ it from ' the statement CIRCLE thus "CircleM". SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor CircleM.x = 150 ' Initial circle's position CircleM.y = 90 CircleM.speed = 1 ' Circle's speed => 1 pixel per loop DO CLS CIRCLE (CircleM.x, CircleM.y), 10, 15 ' According to pushed key we change the circle's coordinates. IF MULTIKEY(SC_RIGHT) THEN CircleM.x = CircleM.x + CircleM.speed IF MULTIKEY(SC_LEFT) THEN CircleM.x = CircleM.x - CircleM.speed IF MULTIKEY(SC_DOWN) THEN CircleM.y = CircleM.y + CircleM.speed IF MULTIKEY(SC_UP) THEN CircleM.y = CircleM.y - CircleM.speed SCREENSYNC 'SCREENLOCK SLEEP 2 ' Prevents 100% CPU usage 'SCREENUNLOCK LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
You will notice I added two more statements in the code. The SETMOUSE statement positions the system mouse cursor(first two parameters) and shows or hides it(third parameter; 0 hides it). You should input this statement with these parameters in every program AFTER the SCREEN statement(IMPORTANT!) by default because even if your program is going to feature a mouse controllable interface you will most likely draw your own cursor. Trust me on this. Uh, I using that line way too often.
The SLEEP statement waits the amount of milliseconds you set and we are having it in our loop to prevent 100% CPU usage. You see, if you don't use that statement any kind of FreeBASIC program with a loop(even the simplest one) will hold up all the computer cycles and make all the other Windows tasks you might be running to crawl. It also makes difficult for you to operate with other tasks while that kind of FreeBASIC program is running. Err...this is not a huge problem and most programmers that have released FreeBASIC games so far did not bother to fix it. Probably the best solution is to leave the usage of the SLEEP statements in the game loops as an option(in a setup menu; later on that). SCREENLOCK and SCREENUNLOCK statements lock the current work page and I REMed them because they are not necessary. Why I inputted them is because while developing Poxie I noticed on places where full screen images were scrolled a lot of trembling and the only way I discovered that prevents this is to use these statements before and after SLEEP or simply to remove SLEEP. Anyway, this is just a hint to you if you in the middle of development of some project notice that kind of problem.
Download the completed example with extra comments inside the source: move_circle.zip
Phew, we are done with the first example. Some of you might think I went into too many details but I feel all this dance was needed to make the next examples and lessons a more enjoyable adventure. God, I suck at writing.
Nevertheless, this example is far from what we want, right? So the next chapter will learn you how to load graphics from external files among other things. Are you excited as I am? :P
Example #2: A warrior running around a green field
In the next example we will be apllying all the knowledge from the first example so don't expect for this example to go into every statement again. I will explain every new statement and just brush off the old ones.
In this section we'll start to code our mini-game which won't be completed in this lesson. In this lesson we'll just create a program where a warrior runs around a green field(single screen).
First I'll show you what graphics we'll be using. We are going to work in 8bit color depth mode so the images that we are going to use need to be saved in that mode(256 colors mode). For warrior sprites I'll use the sprites of the main character from my first game Dark Quest.
As you see this image features 12 sprites of our warrior each 20*20 pixels large. Two for each direction(walk animation) and one sprite for each direction when the warrior is swinging with his sword. Sword swinging won’t be implemented in the first lesson but will become necessary later.
Second image is the background image which you can check/download if you click here(320*200 pixels large, 8bit BMP image).
Download both images and place them where you will place the source or just download the completed example at the end of this section.
On the beginning of our program we should include fbgfx.bi, same as in the first example, and then set the same graphic mode. The code:
#include "fbgfx.bi" SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor
Now we will declare two arrays that will hold our graphics(one for the sprites and one for the background). Think of these arrays as memory buffers where the graphics are stored.
First array we'll name "background1" and declare it with the following line:
DIM SHARED background1(64004) AS INTEGER
Why the number 64004? Because each byte will hold the information about one pixel of the background image(what's the color of that pixel). 320*200 = 64000 which means 64000 pixels. The extra 4 is needed because INTEGER variable itself is 4 bytes long.
The next array we'll declare will hold the graphics of the 12 warrior sprites. We will dimension this array with two elements(dimensions), first representing the number of the sprite saved in the array and second used to store the graphics(each byte holding the information about one pixel) of that sprite(check GFXlib's documentation -> GET statement -> Differences from QB section). I won't go into details why this method is better than using only one dimension. Just have in mind that managing large numbers of sprites or tiles with a single dimension array is much more difficult and “clumsy” especially if the sprites or tiles saved in that array are of different sizes. We'll name this array "WarriorSprite" and declare it with the following line:
DIM SHARED WarriorSprite(12, 404) AS INTEGER
Why the number 404? Because each sprite is 20*20 pixels large. 20*20 = 400 and we add 4(lenght of every INTEGER variable).
Both these lines should be put in the code before the SCREEN statement. That's the way you'll write every program. Subroutine declarations, then variable declarations, then extra subroutine declarations if needed and then the real code. The beginning of our program should now look like this:
#include "fbgfx.bi" DIM SHARED background1(64004) AS INTEGER ' An array that will hold the ' background image DIM SHARED WarriorSprite(12, 404) AS INTEGER ' An array that will hold ' the warior sprites. SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor
After the screen resolution, color depth and number of work pages are set we will hide our work page before loading graphics onto it since we don't want for the user to see all of the program's graphics every time he or she starts our program. To accomplish that we'll use the SCREENSET statement. What it does? It sets the work page(first parameter) and the visible page(second parameter). In our case we will set page 1 as the work page and page 0 as the visible page. After using 'SCREENSET 1, 0' every time we draw or load something on the screen it will be loaded/drawn on the work page and won't be visible to the user until we use the statement SCREENCOPY. This allows us to load graphics onto the screen we don't want for the user to see and delete it before using SCREENCOPY which copies the content of the work page to the visible. This page flipping is also useful in loops with "graphics demanding" programs to avoid flicker or some other unwanted occurrence.
BMP(bitmap) files are loaded in GFXlib with the statement BLOAD. BLOAD can also load BSAVEd images(images saved with the BSAVE statement). BMP images can be loaded directly into an array or onto the screen. When an image is loaded with BLOAD image's associated palette will be set as program's current palette. In 8bit mode all your graphics should be in the same palette. In 16bit and higher color depth modes you don't have to think about palettes. Check GFXlib's documentation for more details on these statements. I have no available information/routines for loading different image formats.
First image we'll load is the background image and we'll load it onto the screen first and then store(capture) that data into an array with the GET statement. I prefer that way over loading images directly into arrays since it's more cleaner and flexible. Also, the second way can lead to errors with multidimensional arrays. The background image is loaded and stored into memory with the following code:
SCREENSET 1, 0 BLOAD "BACKGRND.bmp", 0 GET (0,0)-(319,199), background1(0)
BACKGRND.bmp is the name of the image we are loading. If it's placed in some subdirectory(not where the compiled program is) you need to set a path to it. If, for example, it's placed in the subdirectory "Graphics" you need to replace "BACKGRND.bmp" with "Graphics/BACKGRND.bmp". DON'T USE HARD PATHS in you programs like "C:/FreeBASIC/myprograms/Bobo/BACKGRND.bmp" because that's one of the most stupidest things you can do. It forces the user to extract your program in a specific directory and have death wishes about you. Parameter 0 in the BLOAD statement means we want to load the image onto the screen. Instead of 0 you can put an adress(memory pointer) where to load the image(into an array; refer to GFXlib's documentation). The GET statement in the previous code captures the graphics on the screen from the coordinates (0,0) to (319,199) and stores it into the "background1" array counting from position(byte) 0(input 0 by default).
The second image we'll load is the one with the warrior sprites after which we'll store them into the "WarriorSprite" array. There is 12 sprites and each one is 20*20 pixels large. The code that loads the second image as stores the sprites is as follows:
BLOAD "SPRITES.bmp", 0 GET (0,0)-(19,19), WarriorSprite(1, 0) GET (24,0)-(43,19), WarriorSprite(2, 0) GET (48,0)-(67,19), WarriorSprite(3, 0) GET (72,0)-(91,19), WarriorSprite(4, 0) GET (96,0)-(115,19), WarriorSprite(5, 0) GET (120,0)-(139,19), WarriorSprite(6, 0) GET (144,0)-(163,19), WarriorSprite(7, 0) GET (168,0)-(187,19), WarriorSprite(8, 0) GET (192,0)-(211,19), WarriorSprite(9, 0) GET (216,0)-(235,19), WarriorSprite(10, 0) GET (240,0)-(259,19), WarriorSprite(11, 0) GET (264,0)-(283,19), WarriorSprite(12, 0)
Boy, all that code to store mere 12 sprites! Each GET goes for one sprite and you see how we stored each one on a different position in the array(first element in the "WarriorSprite" array). Now, what if your game features hundreds of sprites and tiles? What to do then? Well, you can apply a form of automation in sprites/tiles capturing if you align(compile) sprites or tiles sequentially in the BMP image. Place them one after another with 0 or more pixels of space between them. You probably noticed a certain order in coordinates used in the twelve GET statements from the last code. That's because these 12 sprites are nicely arranged in the image in one line(from left to right) and with 4 pixels between every sprite. This allows us to load all of them with a single GET statement and a FOR loop. Like this(this replaces the last piece of code):
FOR imagepos = 1 TO 12 GET (0+(imagepos-1)*24,0)-(19+(imagepos-1)*24,19), WarriorSprite(imagepos, 0) NEXT imagepos
If you don't understand how FOR loops work I’ll explain it. A FOR loop simply executes the statement(s) between FOR and NEXT until the variable specified with FOR reaches the number behind TO. In our code in the first cycle(loop) variable imagepos equals 1 and the coordinates in the GET statement are(try to calculate them manually) (0,0) and (19,19). When imagepos equals 12(last cycle) the coordinates are (264,0) and (283,19). So this FOR loop simply "goes through" all the sprites and stores them on the appropriate positions inside the "WarriorSprite" array. Oh, the wonders of FOR loops. Anyway, try to apply this knowledge when construction ways of loading large numbers of tiles and/or sprites from BMP images.
The sprites are now saved on these positions:
WarriorSprite(1, 0) - warrior moving down image #1
WarriorSprite(2, 0) - warrior moving down image #2
WarriorSprite(3, 0) - warrior moving up image #1
WarriorSprite(4, 0) - warrior moving up image #2
WarriorSprite(5, 0) - warrior moving left image #1
WarriorSprite(6, 0) - warrior moving left image #2
WarriorSprite(7, 0) - warrior moving right image #1
WarriorSprite(8, 0) - warrior moving right image #2
WarriorSprite(9, 0) - warrior swinging up
WarriorSprite(10, 0) - warrior swinging down
WarriorSprite(11, 0) - warrior swinging left
WarriorSprite(12, 0) - warrior swinging right
The entire code so far should look like this:
#include "fbgfx.bi" DIM SHARED background1(64004) AS INTEGER ' An array that will hold the ' background image DIM SHARED WarriorSprite(12, 404) AS INTEGER ' An array that will hold ' the warior sprites. SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor ' Let's hide the work page since we are ' going to load program graphics directly ' on the screen. SCREENSET 1, 0 ' Load the background image and store ' it in an array. BLOAD "BACKGRND.bmp", 0 GET (0,0)-(319,199), background1(0) CLS ' Clear our screen since we ' are loading a new image(not ' neccesary but wise). ' Load the sprites onto the screen and store them ' into an array. BLOAD "SPRITES.bmp", 0 FOR imagepos = 1 TO 12 GET (0+(imagepos-1)*24,0)-(19+(imagepos-1)*24,19), WarriorSprite(imagepos, 0) NEXT imagepos
We are finally done with loading graphics. Now we will declare additional variables needed in this example. I'll define a data type like in the previous example but it will contain more variables. The following code should be placed before the image array declarations.
TYPE ObjectType X AS SINGLE Y AS SINGLE Speed AS SINGLE Frame AS INTEGER Direction AS INTEGER Move AS INTEGER Attack AS INTEGER Alive AS INTEGER END TYPE
The object that will be used to control the warrior is declared with:
DIM SHARED Player AS ObjectType
Frame variable will be used to flag the sprite that needs to be displayed(according to warrior's direction, if he is moving or not, etc.). Direction will be used to flag the warrior's direction, Move if he is moving or not, Attack if he is attacking or not(so we could flag the proper sprite) and Alive if he is alive or not(not used in this example but most often necessary).
Let's set a loop on the way it's done in the previous example but also add the SCREENCOPY statement in it since we have previously set the work and the visible page(with 'SCREENSET 1, 0').
DO CLS ' Our "graphics" statement go in here. SCREENCOPY SCREENSYNC SLEEP 2 LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
We should now add a statement in the loop that will draw our warrior. To paste graphics onto the screen with GFXlib we use the PUT statement. It's very simple and works something like this:
PUT (x coordinate, y coordinate), array, Mode
Where "array" is the name of the array where our image/sprite/tile is stored. Mode represents several methods of pasting graphics but at this point you should be aware of two. PSET pastes all the image pixels while TRANS skips pixels that are of background color(transparent background effect). In 16bit and higher color depth modes transparent color is the color RGB 255,0,255(pink; 255 should be the highest value in the palette editor you are using but some drawing programs feature a different number as a maximum color value so have that in mind) while in 8bit mode it's the color 0(first in the palette). Our sprites must be pasted with TRANS or they would be pasted with a black square around them(consisted of color 0). By using MULTIKEY statements according to pushed arrow key we'll change warrior's position and flag the proper direction(with a number). According to direction we will paste the proper sprite. Here we go(entire code):
#include "fbgfx.bi" ' Useful constants(makes your code easier to read and write). const FALSE = 0 const TRUE = 1 DIM SHARED background1(64004) AS INTEGER ' An array that will hold the ' background image DIM SHARED WarriorSprite(12, 404) AS INTEGER ' An array that will hold ' the warior sprites. SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor ' Let's hide the work page since we are ' going to load program graphics directly ' on the screen. SCREENSET 1, 0 ' Load the background image and store ' it in an array. BLOAD "BACKGRND.bmp", 0 GET (0,0)-(319,199), background1(0) CLS ' Clear our screen since we ' are loading a new image(not ' neccesary but wise). ' Load the sprites onto the screen and store them ' into an array. BLOAD "SPRITES.bmp", 0 FOR imagepos = 1 TO 12 GET (0+(imagepos-1)*24,0)-(19+(imagepos-1)*24,19), WarriorSprite(imagepos, 0) NEXT imagepos TYPE ObjectType X AS SINGLE Y AS SINGLE Speed AS SINGLE Frame AS INTEGER Direction AS INTEGER Move AS INTEGER Attack AS INTEGER Alive AS INTEGER END TYPE DIM SHARED Player AS ObjectType ' Warrior's(player's) initial ' position, speed(constant) ' and direction(1 = right) Player.X = 150 Player.Y = 90 Player.Speed = 1 Player.Direction = 1 DO ' Player.Direction = 1 -> warrior moving right ' Player.Direction = 2 -> warrior moving left ' Player.Direction = 3 -> warrior moving down ' Player.Direction = 4 -> warrior moving up Player.Move = FALSE ' By deafult player is not ' moving. ' According to pushed key move the ' player and flag the proper direction. IF MULTIKEY(SC_RIGHT) THEN Player.X = Player.X + Player.Speed Player.Direction = 1 Player.Move = TRUE END IF IF MULTIKEY(SC_LEFT) THEN Player.X = Player.X - Player.Speed Player.Direction = 2 Player.Move = TRUE END IF IF MULTIKEY(SC_DOWN) THEN Player.Y = Player.Y + Player.Speed Player.Direction = 3 Player.Move = TRUE END IF IF MULTIKEY(SC_UP) THEN Player.Y = Player.Y - Player.Speed Player.Direction = 4 Player.Move = TRUE END IF ' According to player's direction flag the ' proper sprite(check in the tutorial on which ' position each sprite is stored). IF Player.Direction = 1 THEN Player.Frame = 7 IF Player.Direction = 2 THEN Player.Frame = 5 IF Player.Direction = 3 THEN Player.Frame = 1 IF Player.Direction = 4 THEN Player.Frame = 3 CLS ' Clear the screen(REM this as get smearing). ' Paste the warrior on Player.X and Player.Y coordinates, ' using sprite number Player.Frame and skip background color. PUT (Player.X, Player.Y), WarriorSprite(Player.Frame, 0), TRANS SCREENCOPY SCREENSYNC SLEEP 2 LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
Take a note of the two constants which I added in the code and which allow us to make our code easier to write and read. Instead of dealing with 0 and 1 as conditions with variables than can only be true or false we say that FALSE in our code will mean 0 and TRUE will mean 1. If you compile this code you'll be able to move the warrior around a black screen(we'll add the background later) but his legs won't move. How to enable "walk" animation? Well, it's very easy. You'll see how the Move variable comes in handy now. You should input this code right after CLS:
Frame1 = (Frame1 MOD 2) + 1 IF Player.Move = FALSE OR Frame1 = 0 THEN Frame1 = 1
The line 'Frame1 = (Frame1 MOD 2) + 1' is a substitute for:
Frame1 = Frame1 + 1 IF Frame1 > 2 THEN Frame1 = 1
You don't have to think on what way the line 'Frame1 = (Frame1 MOD 2) + 1' functions. All you need to know is that is changes variable Frame1 by 1 in each cycle from number 1 to the number specified after MOD. If you want for Frame1 to loop from 50 to 66 you need to input 'Frame1 = (Frame1 MOD 16) + 50' but that's not we need. We need a variable that toggles from 1 to 2 in each cycle to enable walk animation. To make this work we need to change the 4 lines of code preceeding CLS(where according to player's direction the proper sprite is flaged) to this:
IF Player.Direction = 1 THEN Player.Frame = 6 + Frame1 IF Player.Direction = 2 THEN Player.Frame = 4 + Frame1 IF Player.Direction = 3 THEN Player.Frame = 0 + Frame1 IF Player.Direction = 4 THEN Player.Frame = 2 + Frame1
So when Frame1 equals 1 and the player is moving right(Player.Direction = 1) Player.Frame equals 7 while when Frame1 equals 2 Player.Frame is 8. Check the lesson on the place where I specified on which position each sprite is stored in the "WarriorSprite" array. You'll see that “moving right” sprites are stored on positions 7 and 8. Not why we need that condition where the Player.Move variable is used? When the player is not moving Frame1 needs to be 1 or 2 IN EVERY CYCLE(no sprite rotation). Second condition(IF Frame1 = 0) is there to prevent errors(when the loop starts Frame1 might equal 0 and the program might load a sprite out of bounds or something; I highly advise this sort of precaution measure).
If you compile the code again with these changes you'll notice that warrior's legs are moving too fast. How to set the speed of sprite rotation? You need another variable like Frame1(we'll named it Frame2) that will grow to a higher number and connect it with Frame1 so that Frame1 doesn't change in every cycle but only when Frame2 equals a certain number. Check the following code:
Frame2 = (Frame2 MOD 16) + 1 IF Frame2 = 10 THEN Frame1 = (Frame1 MOD 2) + 1
Now Frame1 will change(from 1 to 2 or vice versa) every time Frame2 = 10 and Frame2 will equal 10 every 16 cycles(it grows from 1 to 16 by 1 in every cycle and then drops to 1). We reduced the speed of rotation of the Frame1 variable! Change 16 to some other number to get a different speed of sprite rotation. Using several Frame variables in your code, some connected and some not, will become necessary in larger projects where you will have many objects represented with sprites that need to rotate with different speeds(monsters which walk with different paces, the speed of explosion animations, etc.). With “walking” objects you need to synchronize the speed of that object with sprite rotation(the best you can) or your “walking” object(player, monster, etc.) might seem like it’s sliding or running in place.
One of the last things we'll do in the second example is add a line that pastes the background which is as follows at should be placed before the PUT statement that pastes the warrior:
PUT (0, 0), background1(0), PSET
You can also remove CLS now since in every new cycle of the loop the background is pasted over the entire screen erasing all the graphics from the previous cycle.
Last 4 conditions we'll add in the code are there to prevent the warrior to walk off the screen. Simple conditions but every new line of code should increase your programming experience which you need a lot at this point.
IF Player.X < 0 THEN Player.Move = FALSE Player.X = 0 END IF IF Player.X > 300 THEN Player.Move = FALSE Player.X = 300 END IF IF Player.Y < 0 THEN Player.Move = FALSE Player.Y = 0 END IF IF Player.Y > 180 THEN Player.Move = FALSE Player.Y = 180 END IF
You should be able now to understand this code. Player.Move is changed to FALSE so that the warrior doesn't seem like he is trying to push the edge of the screen. Try to REM these lines('Player.Move = FALSE') and see it yourself.
The FINAL version of the code(for this lesson) looks like this(yippee!):
#include "fbgfx.bi" ' Useful constants(makes your code easier to read and write). const FALSE = 0 const TRUE = 1 DIM SHARED background1(64004) AS INTEGER ' An array that will hold the ' background image DIM SHARED WarriorSprite(12, 404) AS INTEGER ' An array that will hold ' the warior sprites. SCREEN 13,8,2,0 ' Sets the graphic mode SETMOUSE 0,0,0 ' Hides the mouse cursor ' Let's hide the work page since we are ' going to load program graphics directly ' on the screen. SCREENSET 1, 0 ' Load the background image and store ' it in an array. BLOAD "BACKGRND.bmp", 0 GET (0,0)-(319,199), background1(0) CLS ' Clear our screen since we ' are loading a new image(not ' neccesary but wise). ' Load the sprites onto the screen and store them ' into an array. BLOAD "SPRITES.bmp", 0 FOR imagepos = 1 TO 12 GET (0+(imagepos-1)*24,0)-(19+(imagepos-1)*24,19), WarriorSprite(imagepos, 0) NEXT imagepos TYPE ObjectType X AS SINGLE Y AS SINGLE Speed AS SINGLE Frame AS INTEGER Direction AS INTEGER Move AS INTEGER Attack AS INTEGER Alive AS INTEGER END TYPE DIM SHARED Player AS ObjectType ' Warrior's(player's) initial ' position, speed(constant) ' and direction(1 = right) Player.X = 150 Player.Y = 90 Player.Speed = 1 Player.Direction = 1 DO ' Player.Direction = 1 -> warrior moving right ' Player.Direction = 2 -> warrior moving left ' Player.Direction = 3 -> warrior moving down ' Player.Direction = 4 -> warrior moving up Player.Move = FALSE ' By deafult player is not ' moving. ' According to pushed key move the ' player and flag the proper direction. IF MULTIKEY(SC_RIGHT) THEN Player.X = Player.X + Player.Speed Player.Direction = 1 Player.Move = TRUE END IF IF MULTIKEY(SC_LEFT) THEN Player.X = Player.X - Player.Speed Player.Direction = 2 Player.Move = TRUE END IF IF MULTIKEY(SC_DOWN) THEN Player.Y = Player.Y + Player.Speed Player.Direction = 3 Player.Move = TRUE END IF IF MULTIKEY(SC_UP) THEN Player.Y = Player.Y - Player.Speed Player.Direction = 4 Player.Move = TRUE END IF ' The following 4 conditions prevent ' the warrior to walk off the screen. IF Player.X < 0 THEN Player.Move = FALSE Player.X = 0 END IF IF Player.X > 300 THEN Player.Move = FALSE Player.X = 300 END IF IF Player.Y < 0 THEN Player.Move = FALSE Player.Y = 0 END IF IF Player.Y > 180 THEN Player.Move = FALSE Player.Y = 180 END IF ' According to player's direction flag the ' proper sprite(check in the article on which ' position each sprite is stored). IF Player.Direction = 1 THEN Player.Frame = 6 + Frame1 IF Player.Direction = 2 THEN Player.Frame = 4 + Frame1 IF Player.Direction = 3 THEN Player.Frame = 0 + Frame1 IF Player.Direction = 4 THEN Player.Frame = 2 + Frame1 ' Frame1 changes from 1 to 2 or vice versa every ' 16 cycles(set with Frame2 variable). Frame2 = (Frame2 MOD 16) + 1 IF Frame2 = 10 THEN Frame1 = (Frame1 MOD 2) + 1 IF Player.Move = FALSE OR Frame1 = 0 THEN Frame1 = 1 ' Pastes the background. PUT (0, 0), background1(0), PSET ' Pastes the warrior on Player.X and Player.Y coordinates, ' using sprite number Player.Frame and skip background color. PUT (Player.X, Player.Y), WarriorSprite(Player.Frame, 0), TRANS SCREENCOPY SCREENSYNC SLEEP 2 LOOP UNTIL MULTIKEY(SC_Q) OR MULTIKEY(SC_ESCAPE)
Compile it and enjoy.
Download the completed example with the graphics files and extra comments inside the source: move_warrior.zip
Extra stuff: 16bit color depth and example #2
If you want to convert example #2 into 16bit or higher color depth mode you need to convert the two images used in that example to 24bit color depth mode(24 BPP). The best tool for this task is IrfanView which is something anyone who owns a PC must have. With sprites you need to replace the background color 0 with color RGB 255,0,255 like it's done on this image(click on it):
Other changes you need to do is change the screen mode to 14 since modes 13 and lower don't support color depths higher than 8bit. The code:
Since screen mode 14 features a 320*240 pixels large screen we should edit the background image(enlarge it) and dimension the arrays so they can hold the new 24bit graphics. For the background image the new dimension is 320*240*4 + 4 = 307204 (refer to GFXlib's documentation -> GET statement). Also, we should change the screen edge coordinates which prevent the warrior to walk off the screen. Download the entire code with all the changes and new graphics files(16bit version of example #2): move_warrior16bit.zip
If you set the program's color depth to 24bit or 32bit PUT translucency won't work properly. I don't have a lot experience with 16bit and higher color depth graphics so don't ask me to explain this occurrence. I DON'T KNOW! Just set the color depth to 16bit and work with 24bit images. If you have other requirements ask for help in the community forums.
Change the resolution of the 8bit version of example #2 to 640*400, change the screen edges to fit this resolution and don't modify the background image(you are not allowed to enlarge it).
Download homework solution: mw_homework.zip
I simply changed the screen mode to 17(640*400), pasted the background image 4 times on different positions so it would cover the entire screen and changed the screen edges.
What we learned(the most important things):
- How to set a graphic mode
- How to setup a loop
- How the MULTIKEY statement works
- How to declare variables
- How to use variables to enable the movement of a sprite
- How to manage multiple work pages
- How to load graphics from external files and store them into arrays
- How to dimension these arrays(depending on the color depth)
- How to paste graphics stored in arrays
- How to set the speed of variable rotation using the MOD statement
That's all for lesson #1. It was a big one, eh? After finishing "QBasic Tetris Games" I thought I would never write a bigger article. Wasn't I deluded?
I think the first lesson had to be this big. One month is too much for some short lesson to make sense.
I'm not sure when I'll be able to compile the next lesson(these things really take a lot of time to write) but I plan to put a lot of stuff in it. Stuff like subroutine declarations and usage, how to manage NPCs(sheep in our mini-game) and create a simple AI, how to implement particle layers(an REinvention of my own :P), how to use mouse routines(I'll implement a main menu to show this), how to load variables from files and more.
With hopes I'll be able to whip up the next issue in our lifetime, happy coding!
Visit my reviews/articles site: kentauri.digitalblackie.com/articles/