GLBasic tutorial
Contents
- 1 GLBasic tutorial (W.I.P)
- 2 Contents
- 3 First we need a plan
- 4 How to implement the identified objects in a program?
- 5 Enough talk, let's start programming
- 6 Here's the complete code
- 7 And here's the result
- 8 Excercises
- 9 Coming in the next episode
- 10 A new type
- 11 How to use it
- 11.1 Running the new program
- 11.2 What is double buffering
- 11.3 The code
- 11.4 Excercices
- 11.5 In the next episode
- 11.6 Sprites
- 11.7 Plan for the ship
- 11.8 TShip
- 11.9 Changes to the TTorpedo-type
- 11.10 The main program
- 11.11 Complete code so far
- 11.12 What's next
- 11.13 Creating better planets
- 11.14 Gravity
- 11.15 Complete code
- 11.16 What's next?
- 11.17 Adding an opponent
- 11.18 Changes in TShip
- 11.19 How it all looks?
- 11.20 Complete code
- 11.21 Making the game playable
- 11.22 Compile for Pandora
- 11.23 Testing your code
- 11.24 Prepare for distribution
- 11.25 Transfer to Pandora
GLBasic tutorial (W.I.P)
This tutorial is intended for those who would like to learn programming in general and especially writing programs for Pandora. I've decided to use GLBasic because many of us are at least slightly familiar with some dialect of Basic and for beginners it probably is a bit easier language to learn than C or C++. Also, GLBasic is easy to install on a Windows machine and comes with an integrated editor.
I work as a software designer and should know something about programming, but I'm a complete newbie when it comes to GLBasic. I just purchased it a couple of weeks ago, so I will be learning this new language with you. I'll probably make quite a few errors and mistakes, but that happens at work too... Hopefully all errors will be found and fixed.
The goal of this tutorial is to make a simple game. Years ago Amiga had a game called Gravity Wars which I just loved. In that game two space ships shot torpedoes at each other across a playing field filled with planets. Torpedoes were affected by the gravity of the planets and the flight paths of the torpedoes sometimes made full circles around the planets or event the whole playing field.
First_we_need_a_plan GLBasic_tutorial#A_new_type GLBasic_tutorial#Sprites GLBasic_tutorial#Creating_better_planets GLBasic_tutorial#Adding_an_opponent GLBasic_tutorial#Making_the_game_playable GLBasic_tutorial#Compile_for_Pandora
Contents
This tutorial is divided into several small parts
- In part 1 we first make a plan of the game and then create planets.
- In part 2 we shoot some torpedoes.
- Part 3 adds sprites and lets us aim those torpedoes.
- Part 4 adds gravity and nicer planets.
- Part 5 will introduce an opponent and a more scalable way to process input.
- Part 6 will finally make the game actually playable.
- compile! Will go into compiling and publishing the game.
Stay tuned for more parts.
- The last part will tell how to move the finished game to the Pandora. I've not yet any idea how that will be done, since I haven't yet received my Pandora...
Keep in mind the game has not been written yet when I've started writing this tutorial. Hopefully it will be finished some day. Currently it looks like this:
GLBasic Tutorial Part 1
First we need a plan
Before we can start programming anything, we need some kind of plan. It doesn't need to be a comprehensive plan, but spending some time on a plan usually pays off in decreasing the time needed to actually write the program. First we need to describe the program we want to create
- a two player space game
- players shoot each other with torpedoes
- there are planets whose gravity pull the torpedoes towards them. The size and density of the planets differ which affects their gravitational force.
- players need to be able to change the speed and direction of the torpedoes to avoid planets and hit the target
- whoever hits the other player first wins
- in the original game players take turns in shooting each other, but that may change in this version. It might be fun to allow simultaneous shooting.
Now that we have some kind of idea what the resulting game will be like, we need to think what kind of entities or objects the game is made of. Each of the identified objects will have some attributes and actions it can take. Three kinds of objects are fairly easy to find
- planets
- torpedoes
- spaceships
One can think of other objects that might relate for example to the playing field or scoring, but lets keep it simple at first and stick with those three objects.
Planet
Planets could have attributes like location, size, density, mass and look. They don't actually do much except pull the torpedoes towards them.
Torpedo
Torpedo has a location and speed and direction of movement. It also has the ability to blow up spaceships and is pulled towards planets. Also it needs some kind of representation on the screen.
Spaceship
Spaceships in this game have a fixed location, but they can shoot torpedoes in different directions. They also need some kind of representation on the screen. Probably it'd be nice if the two ships didn't look the same.
How to implement the identified objects in a program?
Different programming languages have different kinds of ways to represent objects identified in the plan, quite often an object is described in something called a class. GLBasic doesn't have classes, but it does have something similar called (extended) types. Previously types in GLBasic were like records or structures in other languages, which means they were only a way to group variables together. Since version 8 types have also included functions which makes them much more versatile.
The idea of a type is to collect all variables and functions of an object together in a tight package. The inner workings of an type are of no interest to the other parts of the program, types are used through a set of functions which define its interface to the outside world. For example a program might launch a torpedo to a given direction with given speed and let that torpedo to figure out itself where it should move when it is time to move. Other parts of the program should not be interested how the torpedo calculates its next position when asked to do so, they only need to know that the torpedo is capable of doing it.
Enough talk, let's start programming
First, start up GLBasic Editor, select the uppermost option "Create new project" from the Project Wizard and give you project a name. After that the editor opens with some default comments on the screen. Anything on a line after // is a comment, it doesn't affect the running of the program but is intended for the programmer to describe what that part of the program does. Lets start by creating a type to represent planets.
Type for planets
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(600)+100.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density * self.radius * self.radius * 3.1415 * 0.001 ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: // draw lines radiating outwards from the center of the planet FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE
A type declaration always starts with word TYPE
and the name of the type which in this case is TPlanet
. It's usually a good idea give names which aren't easy to mix up with variable names, that's the reason for using name TPlanet
instead of just Planet
.
TPlanet
has five attributes which describe the planets location, size, density and mass. It also has two functions, one for initializing the attributes of a planet and another for displaying it on the screen. Let's get a closer look
TYPE TPlanet x y radius% density% mass%
These are the variables that describe the attributes of the planet. x
and y
are floating point numbers, that is they can have decimals in them. Variables that have a %
after the name are integer variables and don't have decimals.
// creates a new random sized planet in random coordinates // returns: always 0
Next two lines are comments. They describe what this function does. A function always returns a value to the part of the program which uses it, but this function doesn't specify a return value. That means it always returns zero.
FUNCTION Create:
A function always starts with keyword FUNCTION
followed by the name of the function and a colon.
self.x = RND(600)+100.0 self.y = RND(380)+50.0
These two lines initialize the location of the planet. The keyword self
refers to the type object itself, that is self.x
means the variable x
of this object. RND is a function which returns a random number between 0 and the number given in parenthesis including both. So, the x-coordinate will be between 100 and 700 and y-coordinate between 50 and 430.
// density is 1-4 self.density = RND(3)+1 self.radius = RND(60)+20
Next is another comment and two more random variable initializations.
// lets try this formula first. We'll refine it later if needed. self.mass = self.density * self.radius * self.radius * 3.1415 * 0.001 ENDFUNCTION
Finally there is a preliminary formula which calculates the mass of the planet. That might be changed later, but it's a start. If it is changed, the only change will be here and other parts of the program don't need to know about it.
// returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION
This function gets another planet as an argument and calculates the distance between the centers of the two plantes. Arguments (or parameters, as they are often called) are defined after the colon on the line declaring the function. The function declares three variables which are used locally to calculate the result which is then returned with the RETURN statement.
The main program
If you now try to compile and run the program you only get a blank screen. We have defined a type for planets but the program doesn't actually do anything yet. So let's add a few lines after that type definition.
////////////////////////////////// //// Main program starts here //// ////////////////////////////////// LOCAL planets[] AS TPlanet DIM planets[7]
First there are three comment lines. Then an array called planets
is first declared as a local variable and then its size is set to seven items. Local variables can only be seen in this main program, not in functions and other subroutines. The alternative to local variables are global variables. They can be seen in all parts of the program.
It's a good idea to avoid global variables whenever possible. It may seem easier to declare global variables that can be read and changed everywhere in the program, but it often leads to code which is hard to maintain.
Next line will create a window which is 800 pixels wide and 480 pixels high. Top left corner is in coordinates (0,0) and x-values increase to the right and y-values increase downwards.
SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[])
What was that previous line? That was a function call which passed the recently declared array to the function as a parameter.
// display the created planets SHOWSCREEN
SHOWSCREEN is an essential keyword in GLBasic. Without it nothing you draw or write in the program is displayed in the program window. Next the program waits for a mouse click and then exits.
// wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// ////////////////////////////////
Subroutines
Now it's time for the function we called earlier. The idea behind using functions and subroutines is to keep the main program as simple as possible. Programs should not be made of huge hundreds of lines long routines, instead they should be split into small more manageable bits of code. Whenever a function or subroutine has more than a couple of dozen lines of code it's probably already getting too long and should be split into smaller pieces.
This function begins with a couple of comments and then there is the function declaration which this time has an argument. Since the planets-array was declared local, it can't be accessed directly inside the following function, but it can be accessed by the name aPlanets
used in the argument list
// fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION
First the function needs to declare variables before it can start doing things.
FOR i=0 TO LEN(aPlanets)-1
This line defines a for-loop that start at value zero and ends at LEN(aPlanets)-1
. LEN(aPlanets)
returns the number of items in its parameter array. Since aPlanets
is the parameter of this function and this function is called with the planets
-array as a parameter, it returns 7 in this case. So, the loop could have been written as FOR i=0 TO 6
but the current way is better because it works for all sizes of arrays. If we decide that we need eight planets on the screen instead of seven, we only need to change the array declaration. No change is needed here.
The for-loop means that variable i
is given each value 0,1,2,3,4,5,6 in that order and all lines between the for-line and corresponding next-line is executed for each of those values.
WHILE ok=false
starts another loop which ends with the keyword WEND
. The difference is that there is no loop-variable which gets sequential values. Instead there is a condition which defines how many times WHILE
-loop is executed. Basically the idea here is to loop as long until the planet created is ok.
Inside the WHILE-loop the value for variable ok
is calculated based on the position and size of the planet. If the planet is completely inside the playing area, then it's checked if it overlaps any of the previously created planets. If it is found overlapping another planet, the program exits from the for-loop using a BREAK-statement.
Notice how the innermost for-loop uses the variable defined in the outermost loop:
FOR j=0 TO i-1
This means when i=0, the innermost loop translates to FOR j=0 TO -1 and isn't executed even once. When i=1, the innermost loop becomes FOR j=0 TO 0 and is executed once etc. This way the recently created planet is compared with all the previous ones.
Here's the complete code
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(600)+100.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density * self.radius * self.radius * 3.1415 * 0.001 ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: // draw lines radiating outwards from the center of the planet FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE ////////////////////////////////// //// Main program starts here //// ////////////////////////////////// LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) // display the created planets SHOWSCREEN // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// //////////////////////////////// ///////////////////// //// Subroutines //// ///////////////////// // fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION
And here's the result
Well, it isn't very much, but it's a start. This is not what the planets will look like in the finished game, they are just temporary replacements for the real things.
Excercises
- Change the Draw function so that it draws a circle.
- Change the program so that it creates the planets on a straight line in increasing size.
Coming in the next episode
Next in part 2 we'll create a type for the torpedoes.
GLBasic Tutorial Part 2
A new type
Now that we have planets to fill the playing area, lets next concentrate on torpedoes. The type declaration begins as before with the keyword TYPE and the name of the type. The following lines define the attributes of the torpedo
TYPE TTorpedo // current location curX curY // previous location prevX prevY // movement along each axis dx dy
Next is a function which launches a torpedo. It gets the starting location, direction and speed as arguments.
// Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed ENDFUNCTION
The following function moves the torpedo and draws it on the screen.
// Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDFUNCTION
This last function checks if the torpedo has collided with any of the planets in the argument array.
// Returns true if this torpedo has collided with any of the planets in the aPlanets-array FUNCTION PlanetCollision: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY, result% result =0 // check each planet in the array FOR i=0 TO LEN(aPlanets)-1 // calculate the distance to the center of the planet diffX = aPlanets[i].x-self.curX diffY = self.curY-aPlanets[i].y dist = SQR(diffX*diffX + diffY*diffY) // if it is less than the radius of the planet, we must have hit it IF dist<=aPlanets[i].radius // and the function returns true RETURN TRUE ENDIF NEXT // none of the planets was too close... RETURN FALSE ENDFUNCTION ENDTYPE
How to use it
Adding a new type doesn't yet change what the program does. We also need to use the new type in the main program. Lets add a few lines which create a new torpedo and launch it in 45 degree angle towards upper and right borders of the screen.
////////////////////////////////// //// Main program starts here //// ////////////////////////////////// LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo // 45 degrees is towards right and up torp.Launch(50,200,45,5) // lets do the loop until the torpedo leaves the screen WHILE torp.curX<800 AND torp.curY>0 torp.Move() // Adds a short delay between frames SLEEP 25 SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// ////////////////////////////////
Running the new program
The result isn't quite what was expected.
The torpedo moves on the screen, but the planets only flash briefly on the screen.
What is double buffering
The reason for this result is that GLBasic uses double buffering to reduce screen flicker. That means there are actually two screens. The one that is shown to the user and another one which is used to draw on. Whenever the command SHOWSCREEN is encountered, the two screens are swapped so that the drawing screen becomes the visible screen and the visible screen is cleared and becomes the new drawing screen. That way nothing is ever drawn to the screen which is visible, which makes animations and movement run more smoothly.
Double buffering means we need to draw the planets each time we show the screen to the user. Lets add a short subroutine for that and call it from the main program
// Draw all planets in the argument // returns nothing FUNCTION DrawPlanets: aPlanets[] AS TPlanet FOR i=0 TO LEN(aPlanets)-1 aPlanets[i].Draw() NEXT ENDFUNCTION
Now the result is more like what we wanted to accomplish. The torpedo moves on the screen and the planets are also displayed.
The code
Here's the complete program.
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(600)+100.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density * self.radius * self.radius * 3.1415 * 0.001 ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: // draw lines radiating outwards from the center of the planet FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE TYPE TTorpedo // current location curX curY // previous location prevX prevY // movement along each axis dx dy // Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed ENDFUNCTION // Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDFUNCTION // Returns true if this torpedo has collided with any of the planets in the aPlanets-array FUNCTION PlanetCollision: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY, result% result =0 // check each planet in the array FOR i=0 TO LEN(aPlanets)-1 // calculate the distance to the center of the planet diffX = aPlanets[i].x-self.curX diffY = self.curY-aPlanets[i].y dist = SQR(diffX*diffX + diffY*diffY) // if it is less than the radius of the planet, we must have hit it IF dist<=aPlanets[i].radius // and the function returns true RETURN TRUE ENDIF NEXT // none of the planets was too close... RETURN FALSE ENDFUNCTION ENDTYPE ////////////////////////////////// //// Main program starts here //// ////////////////////////////////// LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo // 45 degrees is towards right and up torp.Launch(50,200,45,5) // lets do the loop until the torpedo leaves the screen WHILE torp.curX<800 AND torp.curY>0 torp.Move() DrawPlanets(planets[]) // Adds a short delay between frames SLEEP 25 SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// //////////////////////////////// ///////////////////// //// Subroutines //// ///////////////////// // fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION // Draw all planets in the argument // returns nothing FUNCTION DrawPlanets: aPlanets[] AS TPlanet FOR i=0 TO LEN(aPlanets)-1 aPlanets[i].Draw() NEXT ENDFUNCTION
Excercices
- There is a command which makes the current screen the default drawing screen. Try what happens when you add command USEASBMP on the line just before SHOWSCREEN.
- Change the program to shoot the torpedo from right to left
- Change the program to shoot two torpedoes
In the next episode
Next in part 3 we will use some sprites and add aiming capability to our torpedoes.
GLBasic Tutorial Part 3
Sprites
Sprites are graphic objects that are "stamped" on the screen over any background graphics. They can be moved, rotated and resized so they are perfect for adding spaceships to our game. Since I really can't draw, I had to use Google's image search to find some nice lookings spaceships. I decided to use some drawn by Kryptid which are available here You can draw your own, if you like.
Next I chose two of the ships I liked and resized the to 50x50 PNGs and rotated them so that they are pointing straight to the right. That's direction 0 degrees.
You could also use BMPs as sprites, but then you have to specify the transparent color with the SETTRANSPARENCY
-command. Transparency information in PNGs is handled automagically.
Plan for the ship
- The ship should be able to display itself in given coordinates and direction.
- It should also be able to turn itself if the player presses the right keys. It would be nice, if the ship turned the faster the longer the player keeps turning.
- Most importantly, the ship should be able to launch torpedoes towards the other player.
TShip
TYPE TShip id x y dir dirChange isTurning torpedoSpeed // Initializes the position and direction of the ship and loads its sprite FUNCTION Init: aID, aX, aY, aDir, aSpriteName$ self.id = aID self.x = aX self.y = aY self.dir = aDir self.isTurning = FALSE self.dirChange = 0 self.torpedoSpeed = 5 LOADSPRITE aSpriteName$, self.id ENDFUNCTION
In the beginning of the type are the attributes of the spaceship followed by an initialization function. The next function changes the direction of the ship and draws it on the screen.
FUNCTION Draw: // if the ship isn't currently turning, zero the change speed // that way the next turning will begin slowly IF NOT self.isTurning self.dirChange = 0 ELSE // turn the ship self.dir = self.dir + self.dirChange // make sure the direction is 0..360 degrees IF self.dir<0 self.dir = self.dir + 360 ELSEIF self.dir>=360 self.dir = self.dir - 360 ENDIF ENDIF // display the sprite of this ship ROTOSPRITE self.id,self.x,self.y,self.dir // reset this indicator. If the player keeps turning the ship, it will be // set again in the TurnXXXXX-functions self.isTurning = FALSE ENDFUNCTION
The following two functions handle the input from the player. The longer the player keeps turning in one direction, the faster the ship turns.
FUNCTION TurnClockwise: IF self.dirChange<0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange < -10 THEN self.dirChange = -15 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = -0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION TurnAnticlockwise: IF self.dirChange>0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange > 10 THEN self.dirChange = 15 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = 0.1 ENDIF self.isTurning = TRUE ENDFUNCTION
The next two functions change the speed used in launching the torpedo. We don't actually use them yet in the program, but they are ready when needed. The allowed speed range of a torpedo is 0 - 10.
FUNCTION IncreaseSpeed: self.torpedoSpeed = self.torpedoSpeed + 0.1 IF self.torpedoSpeed>10 THEN self.torpedoSpeed = 10 ENDFUNCTION FUNCTION DecreaseSpeed: self.torpedoSpeed = self.torpedoSpeed - 0.1 IF self.torpedoSpeed<0 THEN self.torpedoSpeed = 0 ENDFUNCTION
And this final function actually launches a torpedo given as an argument. First it calculates the starting position of the torpedo and then launches it to the same direction the ship is facing.
FUNCTION LaunchTorpedo: aTorpedo AS TTorpedo LOCAL x,y // let's first calculate the center of the ship x = self.x + 24 y = self.y + 24 // then we'll calculate the position of the torpedo from that x = x + COS(self.dir) * 30 y = y - SIN(self.dir) * 30 // now we are ready to fire! aTorpedo.Launch(x,y,self.dir,self.torpedoSpeed) ENDFUNCTION ENDTYPE
Changes to the TTorpedo-type
The TTorpedo-type has been changed a bit. I added an indicator isMoving
which tells if whether the torpedo has been launced and should
be drawn or not. Functions Launch
and Move
have been changed accordingly.
// Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed self.isMoving = TRUE ENDFUNCTION // Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: IF self.isMoving self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDIF ENDFUNCTION
The main program
The main program hasn't been changed much. Now the main loop also draws the ship and checks three keys to see if the ship needs turning or the torpedo is launched. Eventually those lines will be moved to a subroutine, but can be in the main loop for now.
////////////////////////////////// //// Main program starts here //// ////////////////////////////////// SETCURRENTDIR("Media") // sprites are in this directory LIMITFPS 60 // this replaces SLEEP LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo LOCAL ship AS TShip // Lets put ship 1 on the left, facing right ship.Init(1,10,220,0,"Ship1.png") // lets do the loop until the torpedo leaves the screen from top or right WHILE torp.curX<800 AND torp.curY>-1 torp.Move() DrawPlanets(planets[]) ship.Draw() IF KEY(203) THEN ship.TurnAnticlockwise() // left-arrow IF KEY(205) THEN ship.TurnClockwise() // right-arrow IF KEY(28) THEN ship.LaunchTorpedo(torp) // return SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// ////////////////////////////////
You can try turning the ship with arrow keys and shooting torpedoes with return.
Complete code so far
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(600)+100.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density * self.radius * self.radius * 3.1415 * 0.001 ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: // draw lines radiating outwards from the center of the planet FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE TYPE TTorpedo // current location curX curY // previous location prevX prevY // movement along each axis dx dy // has the torpedo been launched isMoving // Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed self.isMoving = TRUE ENDFUNCTION // Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: IF self.isMoving self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDIF ENDFUNCTION // Returns true if this torpedo has collided with any of the planets in the aPlanets-array FUNCTION PlanetCollision: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY, result% result =0 // check each planet in the array FOR i=0 TO LEN(aPlanets)-1 // calculate the distance to the center of the planet diffX = aPlanets[i].x-self.curX diffY = self.curY-aPlanets[i].y dist = SQR(diffX*diffX + diffY*diffY) // if it is less than the radius of the planet, we must have hit it IF dist<=aPlanets[i].radius // and the function returns true self.isMoving = FALSE RETURN TRUE ENDIF NEXT // none of the planets was too close... RETURN FALSE ENDFUNCTION ENDTYPE TYPE TShip id x y dir dirChange isTurning torpedoSpeed // Initializes the position and direction of the ship and loads its sprite FUNCTION Init: aID, aX, aY, aDir, aSpriteName$ self.id = aID self.x = aX self.y = aY self.dir = aDir self.isTurning = FALSE self.dirChange = 0 self.torpedoSpeed = 5 LOADSPRITE aSpriteName$, self.id ENDFUNCTION FUNCTION Draw: // if the ship isn't currently turning, zero the change speed // that way the next turning will begin slowly IF NOT self.isTurning self.dirChange = 0 ELSE // turn the ship self.dir = self.dir + self.dirChange // make sure the direction is 0..360 degrees IF self.dir<0 self.dir = self.dir + 360 ELSEIF self.dir>=360 self.dir = self.dir - 360 ENDIF ENDIF // display the sprite of this ship ROTOSPRITE self.id,self.x,self.y,self.dir // reset this indicator. If the player keeps turning the ship, it will be // set again in the TurnXXXXX-functions self.isTurning = FALSE ENDFUNCTION FUNCTION TurnClockwise: IF self.dirChange<0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange < -10 THEN self.dirChange = -10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = -0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION TurnAnticlockwise: IF self.dirChange>0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange > 10 THEN self.dirChange = 10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = 0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION IncreaseSpeed: self.torpedoSpeed = self.torpedoSpeed + 0.1 IF self.torpedoSpeed>10 THEN self.torpedoSpeed = 10 ENDFUNCTION FUNCTION DecreaseSpeed: self.torpedoSpeed = self.torpedoSpeed - 0.1 IF self.torpedoSpeed<0 THEN self.torpedoSpeed = 0 ENDFUNCTION FUNCTION LaunchTorpedo: aTorpedo AS TTorpedo LOCAL x,y // let's first calculate the center of the ship x = self.x + 24 y = self.y + 24 // then we'll calculate the position of the torpedo from that x = x + COS(self.dir) * 30 y = y - SIN(self.dir) * 30 // now we are ready to fire! aTorpedo.Launch(x,y,self.dir,self.torpedoSpeed) ENDFUNCTION ENDTYPE ////////////////////////////////// //// Main program starts here //// ////////////////////////////////// SETCURRENTDIR("Media") // sprites are in this directory LIMITFPS 60 // this replaces SLEEP LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo LOCAL ship AS TShip // Lets put ship 1 on the left, facing right ship.Init(1,10,220,0,"Ship1.png") // lets do the loop until the torpedo leaves the screen from top or right WHILE torp.curX<800 AND torp.curY>-1 torp.Move() DrawPlanets(planets[]) ship.Draw() IF KEY(203) THEN ship.TurnAnticlockwise() // left-arrow IF KEY(205) THEN ship.TurnClockwise() // right-arrow IF KEY(28) THEN ship.LaunchTorpedo(torp) // return SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// //////////////////////////////// ///////////////////// //// Subroutines //// ///////////////////// // fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION // Draw all planets in the argument // returns nothing FUNCTION DrawPlanets: aPlanets[] AS TPlanet FOR i=0 TO LEN(aPlanets)-1 aPlanets[i].Draw() NEXT ENDFUNCTION
What's next
In part 4 will make the planets prettier and add gravity. You can also return to Part 2 or to the contents of this GLBasic tutorial
GLBasic Tutorial Part 4
Creating better planets
Now it's time to demonstrate how GLBasic's powerful graphics commands make it easy to make our planets look bettter. Sprites are perfect for that too. This time Google's image search lead me to Ironstar Media They have a nice set of planet and asteroids sprites available.
I resized those sprites to 200x200 pixels, which is about the maximum size the game needs.
Those sprites are loaded in a new subroutine which is called from the beginning of the main program. It loads them to sprite numbers 11-14. The LOADBMP
loads an image to be used as a background in the game. I chose a Hubble-telescope image, you can choose any image you like.
SUB LoadGraphics: SETCURRENTDIR("Media") // All graphics are in this directory LOADBMP "Background.png" LOADSPRITE "Planet1.png", 11 LOADSPRITE "Planet2.png", 12 LOADSPRITE "Planet3.png", 13 LOADSPRITE "Planet4.png", 14 ENDSUB
The loaded sprites are then used Draw
-function of the TPlanet-type. First I just added the four new lines in the beginning and left the original lines after that. When I was sure the sprites were displayed on the right place and size I commented out the original lines.
FUNCTION Draw: LOCAL rel = (2.0 * self.radius) / 200.0 LOCAL x = self.x-self.radius / rel LOCAL y = self.y-self.radius / rel ZOOMSPRITE 10+self.density, x , y, rel, rel // draw lines radiating outwards from the center of the planet //FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. // DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) //NEXT ENDFUNCTION
Now the game looks much better!
Gravity
Next we'll add gravity to the game. It's done in the TTorpedo-type's function ObeyGravity which looks like this:
// Changes the current direction and speed of this torpedo according to the gravities of the planets in the // aPlanets array. returns nothing FUNCTION ObeyGravity: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY FOR i=0 TO LEN(aPlanets)-1 // calculate the difference between x-coordinates diffX = aPlanets[i].x-self.curX // and y-coordinates diffY = self.curY-aPlanets[i].y // then the distance is squareroot of the sum of squares dist = SQR(diffX*diffX + diffY*diffY) // then calculate the gravitational pull of a planet along each axis fx = (diffX/50) * aPlanets[i].mass / dist fy = (diffY/50) * aPlanets[i].mass / dist // change the change of coordinates accordingly self.dx = self.dx + fx self.dy = self.dy + fy NEXT ENDFUNCTION
That's the current version anyway, it may need some tweaking later. I already changed the way a planet's mass is calculated a bit.
That function is called from the main program, which currently looks like this.
GOSUB LoadGraphics LIMITFPS 60 // this replaces SLEEP LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo LOCAL ship AS TShip // Lets put ship 1 on the left, facing right ship.Init(1,10,220,0,"Ship1.png") // lets do the loop until the torpedo leaves the screen from top or right WHILE torp.curX<800 AND torp.curY>-1 IF torp.isMoving torp.Move() torp.ObeyGravity(planets[]) IF torp.PlanetCollision(planets[]) torp.isMoving = FALSE ENDIF ENDIF DrawPlanets(planets[]) ship.Draw() IF KEY(203) THEN ship.TurnAnticlockwise() // left-arrow IF KEY(205) THEN ship.TurnClockwise() // right-arrow IF KEY(28) THEN ship.LaunchTorpedo(torp) // return SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END
When you try shooting torpedoes now, the planets' gravitational pull affect the flight path.
Complete code
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(600)+100.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60-self.density*6)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density/2.0 * self.radius/50.0 * self.radius/50.0 * 3.1415 PRINT self.mass, self.x,self.y ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: LOCAL rel = (2.0 * self.radius) / 200.0 LOCAL x = self.x-self.radius / rel LOCAL y = self.y-self.radius / rel ZOOMSPRITE 10+self.density, x , y, rel, rel // draw lines radiating outwards from the center of the planet //FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. // DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) //NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE TYPE TTorpedo // current location curX curY // previous location prevX prevY // movement along each axis dx dy // has the torpedo been launched isMoving // Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed self.isMoving = TRUE ENDFUNCTION // Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: IF self.isMoving self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDIF ENDFUNCTION // Returns true if this torpedo has collided with any of the planets in the aPlanets-array FUNCTION PlanetCollision: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY, result% result =0 // check each planet in the array FOR i=0 TO LEN(aPlanets)-1 // calculate the distance to the center of the planet diffX = aPlanets[i].x-self.curX diffY = self.curY-aPlanets[i].y dist = SQR(diffX*diffX + diffY*diffY) // if it is less than the radius of the planet, we must have hit it IF dist<=aPlanets[i].radius // and the function returns true self.isMoving = FALSE RETURN TRUE ENDIF NEXT // none of the planets was too close... RETURN FALSE ENDFUNCTION // Changes the current direction and speed of this torpedo according to the gravities of the planets in the // aPlanets array. returns nothing FUNCTION ObeyGravity: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY FOR i=0 TO LEN(aPlanets)-1 // calculate the difference between x-coordinates diffX = aPlanets[i].x-self.curX // and y-coordinates diffY = self.curY-aPlanets[i].y // then the distance is squareroot of the sum of squares dist = SQR(diffX*diffX + diffY*diffY) // then calculate the gravitational pull of a planet along each axis fx = (diffX/50) * aPlanets[i].mass / dist fy = (diffY/50) * aPlanets[i].mass / dist // change the change of coordinates accordingly self.dx = self.dx + fx self.dy = self.dy + fy NEXT ENDFUNCTION ENDTYPE TYPE TShip id x y dir dirChange isTurning torpedoSpeed // Initializes the position and direction of the ship and loads its sprite FUNCTION Init: aID, aX, aY, aDir, aSpriteName$ self.id = aID self.x = aX self.y = aY self.dir = aDir self.isTurning = FALSE self.dirChange = 0 self.torpedoSpeed = 5 LOADSPRITE aSpriteName$, self.id ENDFUNCTION FUNCTION Draw: // if the ship isn't currently turning, zero the change speed // that way the next turning will begin slowly IF NOT self.isTurning self.dirChange = 0 ELSE // turn the ship self.dir = self.dir + self.dirChange // make sure the direction is 0..360 degrees IF self.dir<0 self.dir = self.dir + 360 ELSEIF self.dir>=360 self.dir = self.dir - 360 ENDIF ENDIF // display the sprite of this ship ROTOSPRITE self.id,self.x,self.y,self.dir // reset this indicator. If the player keeps turning the ship, it will be // set again in the TurnXXXXX-functions self.isTurning = FALSE ENDFUNCTION FUNCTION TurnClockwise: IF self.dirChange<0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange < -10 THEN self.dirChange = -10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = -0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION TurnAnticlockwise: IF self.dirChange>0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange > 10 THEN self.dirChange = 10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = 0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION IncreaseSpeed: self.torpedoSpeed = self.torpedoSpeed + 0.1 IF self.torpedoSpeed>10 THEN self.torpedoSpeed = 10 ENDFUNCTION FUNCTION DecreaseSpeed: self.torpedoSpeed = self.torpedoSpeed - 0.1 IF self.torpedoSpeed<0 THEN self.torpedoSpeed = 0 ENDFUNCTION FUNCTION LaunchTorpedo: aTorpedo AS TTorpedo LOCAL x,y // let's first calculate the center of the ship x = self.x + 24 y = self.y + 24 // then we'll calculate the position of the torpedo from that x = x + COS(self.dir) * 30 y = y - SIN(self.dir) * 30 // now we are ready to fire! aTorpedo.Launch(x,y,self.dir,self.torpedoSpeed) ENDFUNCTION ENDTYPE ////////////////////////////////// //// Main program starts here //// ////////////////////////////////// GOSUB LoadGraphics LIMITFPS 60 // this replaces SLEEP LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torp AS TTorpedo LOCAL ship AS TShip // Lets put ship 1 on the left, facing right ship.Init(1,10,220,0,"Ship1.png") // lets do the loop until the torpedo leaves the screen from top or right WHILE torp.curX<800 AND torp.curY>-1 IF torp.isMoving torp.Move() torp.ObeyGravity(planets[]) IF torp.PlanetCollision(planets[]) torp.isMoving = FALSE ENDIF ENDIF DrawPlanets(planets[]) ship.Draw() IF KEY(203) THEN ship.TurnAnticlockwise() // left-arrow IF KEY(205) THEN ship.TurnClockwise() // right-arrow IF KEY(28) THEN ship.LaunchTorpedo(torp) // return SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// //////////////////////////////// ///////////////////// //// Subroutines //// ///////////////////// // fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION // Draw all planets in the argument // returns nothing FUNCTION DrawPlanets: aPlanets[] AS TPlanet FOR i=0 TO LEN(aPlanets)-1 aPlanets[i].Draw() NEXT ENDFUNCTION SUB LoadGraphics: SETCURRENTDIR("Media") // All graphics are in this directory LOADBMP "Background.png" LOADSPRITE "Planet1.png", 11 LOADSPRITE "Planet2.png", 12 LOADSPRITE "Planet3.png", 13 LOADSPRITE "Planet4.png", 14 ENDSUB
What's next?
In the next part of this tutorial we'll add another ship to the game and make some changes to the input-handling.
GLBasic Tutorial Part 5
Adding an opponent
This time we'll add another ship to the screen. Let's take a look at the new main program first. The beginning hasn't changed, but the initialization of ships and torpedoes is a bit different.
LOCAL torps[] AS TTorpedo DIM torps[2] LOCAL ships[] AS TShip DIM ships[2] // Lets put ship 1 on the left, facing right ships[0].Init(1,10,220,0,"Ship1.png") ships[0].SetControls(30,32,17,45,29) // and ship 2 on the right, facing left ships[1].Init(2,740,220,180,"Ship2.png") ships[1].SetControls(203,205,200,208,156)
The two ships and torpedoes are in an arrays and not separate variables. That way we don't need to handle each ship separately. Instead we can use one loop to handle both ships. In order to be able to control the movement of the ships in a loop, each ship needs to know which keys turn it and which keys are used for controlling the torpedo speed and launch. That's what the new function SetControls does. You can see it's implementation in the complete code below.
An an added bonus we could add more than two ships to the array and the main program loop wouldn't need any changes. So, it's better to use arrays than separate variables when the program needs to handle several objects of the same type. The main loop comes next:
// lets do the loop until the user presses esc WHILE TRUE // move each torpedo if necessary FOR i =0 TO LEN(torps[])-1 IF torps[i].isMoving torps[i].Move() torps[i].ObeyGravity(planets[]) // if the torpedo is moving, check for collisions IF torps[i].PlanetCollision(planets[]) torps[i].isMoving = FALSE ENDIF ENDIF NEXT DrawPlanets(planets[]) // draw each ship and handle user input FOR i=0 TO LEN(ships[])-1 ships[i].Draw() ships[i].CheckInput() IF ships[i].isShooting THEN ships[i].LaunchTorpedo(torps[i]) NEXT SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END
First the main loop moves all the torpedoes in the array, then draws the planets and finally draws the ships and checks for user input.
Changes in TShip
There are new properties in TShip which tell the keys that are used to turn the ship and change the speed of the torpedo and launch it. Those variables are set by the SetControls-function. There is also another new property named isShooting, which is true when the player has pressed the fire-button. The correct keycodes can be determined by the GLBasic tool Keycodes which can be found in the Tools menu of the editor.
// Set the keycodes to control the ship FUNCTION SetControls: aLeft, aRight, aUp, aDown, aShoot self.left = aLeft self.right = aRight self.up = aUp self.down = aDown self.shoot = aShoot ENDFUNCTION
The new variables are used in the CheckInput-function.
FUNCTION CheckInput: IF KEY(self.left) THEN self.TurnAnticlockwise() IF KEY(self.right) THEN self.TurnClockwise() IF KEY(self.up) THEN self.IncreaseSpeed() IF KEY(self.down) THEN self.DecreaseSpeed() self.isShooting = KEY(self.shoot) ENDFUNCTION
There are also small changes in the Draw-function which now display the direction and the speed of the torpedo below the ship. The LaunchTorpedo has been changed not to launch again an already moving torpedo. You can see all the changes in the complete code below.
How it all looks?
If you look closely, you can see two torpedoes flying on the screen...
Complete code
TYPE TPlanet x y radius% density% mass% // creates a new random sized planet in random coordinates // returns: always false FUNCTION Create: self.x = RND(500)+150.0 self.y = RND(380)+50.0 // density is 1-4 self.density = RND(3)+1 self.radius = RND(60-self.density*6)+20 // lets try this formula first. We'll refine it later if needed. self.mass = self.density/2.0 * self.radius/50.0 * self.radius/50.0 * 3.1415 PRINT self.mass, self.x,self.y ENDFUNCTION // display the planet // returns: always false FUNCTION Draw: LOCAL rel = (2.0 * self.radius) / 200.0 LOCAL x = self.x-self.radius / rel LOCAL y = self.y-self.radius / rel ZOOMSPRITE 10+self.density, x , y, rel, rel // draw lines radiating outwards from the center of the planet //FOR i=0 TO 360 STEP 36 / self.density // the colour of the line depends on the density of the planets. // DRAWLINE self.x,self.y, self.x+COS(i)*self.radius, self.y-SIN(i)*self.radius, RGB(self.density*60,255-self.density*40, 140+self.density*30) //NEXT ENDFUNCTION // returns true if this planet overlaps with the parameter FUNCTION Overlaps: aOther AS TPlanet LOCAL dx, dy, dist dx = self.x - aOther.x dy = self.y - aOther.y dist = SQR(dx*dx + dy*dy) // two planets overlap if the distance between their centers is less than the sum of their radiuses RETURN dist <= (self.radius + aOther.radius) ENDFUNCTION ENDTYPE TYPE TTorpedo // current location curX curY // previous location prevX prevY // movement along each axis dx dy // has the torpedo been launched isMoving // Initializes the attributes of a torpedo. // aX AND aY are the location // aDir is the direction between 0 and 360 degrees. 0 is to the right, 90 straight up, etc. // aSpeed is the launch speed // returns nothing FUNCTION Launch: aX,aY, aDir, aSpeed self.curX = aX self.prevX = aX self.curY = aY self.prevY = aY // calculate the movement speed along x- and y-axis self.dx = COS(aDir) * aSpeed self.dy = SIN(aDir) * aSpeed self.isMoving = TRUE ENDFUNCTION // Moves the torpedo one step to the current direction with current speed and draws a line on the screen // returns nothing FUNCTION Move: IF self.isMoving self.prevX = self.curX self.prevY = self.curY self.curX = self.curX + self.dx self.curY = self.curY - self.dy DRAWLINE self.prevX,self.prevY, self.curX,self.curY, RGB(255,255,255) ENDIF ENDFUNCTION // Returns true if this torpedo has collided with any of the planets in the aPlanets-array FUNCTION PlanetCollision: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY, result% result =0 // check each planet in the array FOR i=0 TO LEN(aPlanets)-1 // calculate the distance to the center of the planet diffX = aPlanets[i].x-self.curX diffY = self.curY-aPlanets[i].y dist = SQR(diffX*diffX + diffY*diffY) // if it is less than the radius of the planet, we must have hit it IF dist<=aPlanets[i].radius // and the function returns true self.isMoving = FALSE RETURN TRUE ENDIF NEXT // none of the planets was too close... RETURN FALSE ENDFUNCTION // Changes the current direction and speed of this torpedo according to the gravities of the planets in the // aPlanets array. returns nothing FUNCTION ObeyGravity: aPlanets[] AS TPlanet LOCAL i%, fx,fy, dist, diffX, diffY FOR i=0 TO LEN(aPlanets)-1 // calculate the difference between x-coordinates diffX = aPlanets[i].x-self.curX // and y-coordinates diffY = self.curY-aPlanets[i].y // then the distance is squareroot of the sum of squares dist = SQR(diffX*diffX + diffY*diffY) // then calculate the gravitational pull of a planet along each axis fx = (diffX/50) * aPlanets[i].mass / dist fy = (diffY/50) * aPlanets[i].mass / dist // change the change of coordinates accordingly self.dx = self.dx + fx self.dy = self.dy + fy NEXT ENDFUNCTION FUNCTION LostInSpace: ENDFUNCTION ENDTYPE TYPE TShip id x y dir dirChange isTurning torpedoSpeed isShooting left% right% up% down% shoot% // Initializes the position and direction of the ship and loads its sprite FUNCTION Init: aID, aX, aY, aDir, aSpriteName$ self.id = aID self.x = aX self.y = aY self.dir = aDir self.isTurning = FALSE self.dirChange = 0 self.torpedoSpeed = 5 LOADSPRITE aSpriteName$, self.id ENDFUNCTION // Set the keycodes to control the ship FUNCTION SetControls: aLeft, aRight, aUp, aDown, aShoot self.left = aLeft self.right = aRight self.up = aUp self.down = aDown self.shoot = aShoot ENDFUNCTION FUNCTION CheckInput: IF KEY(self.left) THEN self.TurnAnticlockwise() IF KEY(self.right) THEN self.TurnClockwise() IF KEY(self.up) THEN self.IncreaseSpeed() IF KEY(self.down) THEN self.DecreaseSpeed() self.isShooting = KEY(self.shoot) ENDFUNCTION FUNCTION Draw: // if the ship isn't currently turning, zero the change speed // that way the next turning will begin slowly IF NOT self.isTurning self.dirChange = 0 ELSE // turn the ship self.dir = self.dir + self.dirChange // make sure the direction is 0..360 degrees IF self.dir<0 self.dir = self.dir + 360 ELSEIF self.dir>=360 self.dir = self.dir - 360 ENDIF ENDIF // display the sprite of this ship ROTOSPRITE self.id,self.x,self.y,self.dir // display the direction of the ship PRINT FORMAT$(6, 2, self.dir), self.x, self.y + 55 // display shot speed DRAWLINE self.x, self.y+64, self.x + 5*self.torpedoSpeed, self.y+64, RGB(255,255,255) // reset this indicator. If the player keeps turning the ship, it will be // set again in the TurnXXXXX-functions self.isTurning = FALSE ENDFUNCTION FUNCTION TurnClockwise: IF self.dirChange<0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange < -10 THEN self.dirChange = -10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = -0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION TurnAnticlockwise: IF self.dirChange>0 // The ship was already turning this way. Let's increase the turning speed self.dirChange = self.dirChange * 1.1 IF self.dirChange > 10 THEN self.dirChange = 10 ELSE // The ship was turning to the other direction. Let's start with a slow speed self.dirChange = 0.1 ENDIF self.isTurning = TRUE ENDFUNCTION FUNCTION IncreaseSpeed: self.torpedoSpeed = self.torpedoSpeed + 0.1 IF self.torpedoSpeed>10 THEN self.torpedoSpeed = 10 ENDFUNCTION FUNCTION DecreaseSpeed: self.torpedoSpeed = self.torpedoSpeed - 0.1 IF self.torpedoSpeed<0 THEN self.torpedoSpeed = 0 ENDFUNCTION FUNCTION LaunchTorpedo: aTorpedo AS TTorpedo LOCAL x,y // you can't launch an already moving torpedo IF NOT aTorpedo.isMoving // let's first calculate the center of the ship x = self.x + 24 y = self.y + 24 // then we'll calculate the position of the torpedo from that x = x + COS(self.dir) * 30 y = y - SIN(self.dir) * 30 // now we are ready to fire! aTorpedo.Launch(x,y,self.dir,self.torpedoSpeed) ENDIF ENDFUNCTION ENDTYPE ////////////////////////////////// //// Main program starts here //// ////////////////////////////////// GOSUB LoadGraphics LIMITFPS 60 // this replaces SLEEP LOCAL planets[] AS TPlanet DIM planets[7] SETSCREEN 800,480,0 // create and draw the planets CreatePlanets(planets[]) LOCAL torps[] AS TTorpedo DIM torps[2] LOCAL ships[] AS TShip DIM ships[2] // Lets put ship 1 on the left, facing right ships[0].Init(1,10,220,0,"Ship1.png") ships[0].SetControls(30,32,17,45,29) // and ship 2 on the right, facing left ships[1].Init(2,740,220,180,"Ship2.png") ships[1].SetControls(203,205,200,208,156) // lets do the loop until the user presses esc WHILE TRUE // move each torpedo if necessary FOR i =0 TO LEN(torps[])-1 IF torps[i].isMoving torps[i].Move() torps[i].ObeyGravity(planets[]) // if the torpedo is moving, check for collisions IF torps[i].PlanetCollision(planets[]) torps[i].isMoving = FALSE ENDIF ENDIF NEXT DrawPlanets(planets[]) // draw each ship and handle user input FOR i=0 TO LEN(ships[])-1 ships[i].Draw() ships[i].CheckInput() IF ships[i].isShooting THEN ships[i].LaunchTorpedo(torps[i]) NEXT SHOWSCREEN WEND // wait for the user to click a mousebutton MOUSEWAIT // exit program END //////////////////////////////// //// Main program ends here //// //////////////////////////////// ///////////////////// //// Subroutines //// ///////////////////// // fills the parameter array with planets which do not overlap and are completely on the playing area // returns: always false FUNCTION CreatePlanets: aPlanets[] AS TPlanet LOCAL ok LOCAL i%, j% // the first index of the array is zero and the last is length of the array - 1 FOR i=0 TO LEN(aPlanets)-1 // lets initialize this to false ok = FALSE // now the while-loop is entered at least once WHILE ok=FALSE // lets create a random planet aPlanets[i].Create() // created planet must be completely inside the playing area. We are now using numbers to define the area, which isn't // the best possible solution and will probably change later. ok=aPlanets[i].x-aPlanets[i].radius>50 AND aPlanets[i].x+aPlanets[i].radius<750 AND aPlanets[i].y-aPlanets[i].radius>15 AND aPlanets[i].y+aPlanets[i].radius<465 // planets must not overlap. No need to check if the planet was already found unsuitable IF ok // compare the new planet to each of the previously created planets FOR j=0 TO i-1 IF aPlanets[j].Overlaps(aPlanets[i]) ok=FALSE // if it overlaps even one other planet, no need to check others BREAK ENDIF NEXT ENDIF // go back to the beginning of the while-loop is ok is false WEND // display the created planet aPlanets[i].Draw() NEXT ENDFUNCTION // Draw all planets in the argument // returns nothing FUNCTION DrawPlanets: aPlanets[] AS TPlanet FOR i=0 TO LEN(aPlanets)-1 aPlanets[i].Draw() NEXT ENDFUNCTION SUB LoadGraphics: SETCURRENTDIR("Media") // All graphics are in this directory LOADBMP "Background.png" LOADSPRITE "Planet1.png", 11 LOADSPRITE "Planet2.png", 12 LOADSPRITE "Planet3.png", 13 LOADSPRITE "Planet4.png", 14 ENDSUB
GLBasic Tutorial Part 6
Making the game playable
So far we've been able to turn the ships and shoot torpedoes. That can be amusing, especially when the flight path takes several circles around the planets. But now it's finally time to add some collision detection between the ships and the torpedoes.
This part will be completed in the near future.
GLBasic Publish
Compile for Pandora
To compile for the Pandora, go to the top menu and select Compiler->Build-Multiplatform (Shift+F8). Select Pandora and click OK. This will give you a new folder inside your source-code folder called .\project_name\distribute\Pandora\. Here is the PND located but it does not contain a properly filled in PXML.xml so this you will have to provide yourself.
Testing your code
If you go one folder deeper into the distribution folder (.\project_name\distribute\Pandora\project_name\) you will find all the files needed for distribution. The PXML.xml file needs some editing to work for you since it contains info that is related to your games name, description and so on.
The PXML page has some examples of different PXML.xml files at the bottom of the page.
I used this one. Just change it to fit your game.
Make sure you change the name of the exe file (pandora-exe.pnd) into "pandora-exe" and update the PXML-xml accordingly (the exec tag).
For testing I recommend you to copy the whole folder into /pandora/apps/project_name/ folder on the pandora SD card and it will show up on your desktop (sometimes) and in the menu on the category you selected in the PXML.xml file (sometimes). Why this does not work every time I have no idea but a reboot of the pandora usually fixes this. When it finally shows up you don't have to fiddle any more with it and it shows up every time as long as you just copy the files replacing the old ones.
Prepare for distribution
To upload your game or application to the archives you need to make sure the PXML.xml file is just like you want it. Look in the mini menu if it shows up in the right place of categories and so on. Also give it a nice icon (pandoraicon.png).
Now you need to convert it all into a .pnd file. Download PNDTools and start it.
Drag and drop all your files from the folder .\project_name\distribute\Pandora\project_name\ into the upper white area where it says "drag & drop stuff here". Make sure you don't copy the "project_name" folder, only the contents so the PXML.xml file is in the root of the .pnd file. PNDTools will ask you if you want it to include the PXML.xml file into the PND properly, just answer Yes.
If PNDTools also asks why you are trying to add a PND file to a PND file, you forgot to change the file-ending of the exe file above.
Click the "Load icon..." button and select the previously created icon.
Now everything is prepared and all you need to do is click "Create PND..." and select where to save it.
Transfer to Pandora
The easiest way to transfer everything to the Pandora is SSH file transfer. It's quite slow but as long as you only transfer the exe file for testing, it's fast enough. Set it up according to the manual. (todo: find the page again :-)
Second option is to just pop the SD Card into your computer again and transfer the normal way. This is ofcourse much faster than SSH over wireless. Make sure you put the folder or .pnd file into the /pandora/apps/ folder since this gives a higher chance that it actually shows up either on the desktop or in the menu. Also don't forget to remove the test-folder if you are uploading the final .pnd file. I'm not sure what happens if they are both present at the same time.