How to Program a Game: Lesson 1
Introduction by Lachie Dazdarian
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. I'm using here the word (well, it's an acronym) "BASIC" and not FreeBASIC, because if you know the basics of QuickBASIC, Visual BASIC or any other variant of BASIC, these lessons should be easy to comprehend.
I'm starting this series because I feel that tutorials of this kind were always something what our community was lacking, even before FreeBASIC. I've corresponded during my programming lifetime with quite 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 acquire the actual skill lasts for 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 this 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 / tools you are using.
The example programs and mini-games we'll create will be coded in GFXlib (the FreeBASIC's built-in graphics library). Lynn's Legacy, ArKade, Mighty Line and Poxie were coded in it (among many others), and I think those games are good references. But don't worry. Switching from one graphics library to another is relatively easy when you know how to code in at least one.
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.18b), and one of the FreeBASIC IDEs available. I recommend FBIDE or FBEdit.
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 the 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). This document is somewhat outdated as FreeBASIC moved on with new versions, so be sure to refer to this online FreeBASIC manual too (part of the FreeBASIC Wiki).
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 (8-bit, 16-bit, ...). For example, 8-bit color depth is the standard 256 colors mode (8 bits per pixel). The graphic mode is set with the SCREEN statement like this:
Screen 13,8,2,0
13 means 320*200 graphic resolution, 8 means 8-bit 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 or FreeBASIC Wiki (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 foundation of any program, not just a computer game. Coding a program on a way it would stop/halt 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 play. 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 statements here later
Loop Until Inkey$ = "Q" Or Inkey$ = "q"
Do
' We'll put our statements 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"
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 its coordinates with VARIABLES. For this we'll use two variables named circlex and circley. Check the following code:
Dim Shared As Single circlex, circley
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"
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.
We declared first two variables in our program. Since FreeBASIC ver.0.17 all variables in FreeBASIC programs MUST be declared, although if you use -lang qb command line during compiling you can compile using old QBasic compatibility dialect (I don't recommend it as it will keep you deprived of possible advances and extensions which default FB compatibility already provides and will provide). For more info on this check the appropriate page of the FreeBASIC wiki - Using the command line. Variables are declared (dimensioned) on this way:
Dim variable_name [As type_of_variable]
Or...
Dim [As type_of_variable] variable1, variable2, variable3, ...
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 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 or DOUBLE. 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 effects 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 ARRAYS inside a subroutine, I advise you to replace DIM with REDIM. Strings are used to hold text data. Like YourName = "Dodo", but you need to declare YourName AS STRING first.
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. GFXlib allows you to replace these scancodes with "easy to read" constants like it's explained in Appendix A. To use GFXlib you need to include its .bi file (fbgfx.bi) into your source. 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. The code you need to add are these two lines as it follows:
#include "fbgfx.bi"
Using FB
Using FB
It's best to put these two lines 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 the /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. Using FB tells the program that we will be accessing GFXlib symbols without namespace, meaning, without having to put 'FB.' in front of every GFXlib symbol. Refer to FreeBASIC Wiki on USING.
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 the 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"
Using FB
Dim Shared As Single circlex, circley, circlespeed
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)
Using FB
Dim Shared As Single circlex, circley, circlespeed
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 the 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 SLEEP command. What it does? It waits until the specified amount of time has elapsed (in milliseconds) or a key is pressed. To escape the key press option use SLEEP milliseconds, 1. This statement is also an efficient solution for the 100 % CPU usage problem. 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 a fair amount of programmers that have released FreeBASIC games so far did not bother to fix it.
Copy and paste the following code and compile it:
#include "fbgfx.bi"
Using FB
Dim Shared As Single circlex, circley, circlespeed
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
Sleep 10, 1
Loop Until MultiKey(SC_Q) Or MultiKey(SC_ESCAPE)
Using FB
Dim Shared As Single circlex, circley, circlespeed
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
Sleep 10, 1
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 our 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.
The way variables are declared in the above code is not the most convenient 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
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".
' 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 from 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"
Using FB
' 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
Sleep 10, 1 ' Wait for 10 milliseconds.
Loop Until MultiKey(SC_Q) Or MultiKey(SC_ESCAPE)
Using FB
' 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
Sleep 10, 1 ' Wait for 10 milliseconds.
Loop Until MultiKey(SC_Q) Or MultiKey(SC_ESCAPE)
You will notice I added one more statement 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.
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.
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.
Example 2: A warrior running around a green field
In the next example we will be applying 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 8-bit 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.
http://hmcsoft.org/fb/htpagl1-sprites.png
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, 8-bit 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"
Using FB
Screen 13,8,2,0 ' Sets the graphic mode
SetMouse 0,0,0 ' Hides the mouse cursor
Using FB
Screen 13,8,2,0 ' Sets the graphic mode
SetMouse 0,0,0 ' Hides the mouse cursor
Now we will declare two memory pointers that will point to memory buffers where our graphics will be stored (one for the sprites and one for the background).
The first pointer we'll name background1 and declare it with the following line:
Dim Shared background1 As Any Ptr
ANY PTR tells us that background1 will actually be a memory pointer. A pointer defined as an ANY PTR disables the compiler checking for the type of data it points to. It is useful as it can point to different types of data. We'll use pointers because we will allocate memory for our graphics using the IMAGECREATE statement. IMAGECREATE allocates the right amount of memory for a piece of graphics (sprite/image) if we input its height and width. Otherwise we would have to do it manually, meaning, calculate the needed amount of memory as the result of the sprite size, bit-depth and variable size. IMAGECREATE does this for use. As IMAGECREATE results with a pointer, we need to refer a pointer to it and not a variable. Don't worry if you don't know anything about pointers. You don't need to (to comprehend this tutorial).
The next pointer we'll declare will point to the memory buffer that holds the 12 warrior sprites. We will dimension this pointer as a single dimension array, each element in the array representing one sprite.
Dim Shared WarriorSprite(12) As Any Ptr
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"
Using FB
Dim Shared background1 As Any Ptr ' A pointer that points to a memory
' buffer holding the background graphics
Dim Shared WarriorSprite(12) As Any Ptr ' A pointer that points to a memory
' buffer holding the warrior sprites
Screen 13,8,2,0 ' Sets the graphic mode
SetMouse 0,0,0 ' Hides the mouse cursor
Using FB
Dim Shared background1 As Any Ptr ' A pointer that points to a memory
' buffer holding the background graphics
Dim Shared WarriorSprite(12) As Any Ptr ' A pointer that points to a memory
' buffer holding the warrior 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 or SCREENSET with different parameters (SCREENSET 1, 1). This allows us to load graphics onto the screen we don't want for the user to see and delete it before coping the content on the work page to the visible page. This page flipping is also useful in loops with "graphics demanding" programs to avoid flicker or some other unwanted occurrence.