Difference between revisions of "GLBasic tutorial"

From Pandora Wiki
Jump to: navigation, search
(Contents)
 
(7 intermediate revisions by 4 users not shown)
Line 6: Line 6:
  
 
The goal of this tutorial is to make a simple game. Years ago Amiga had a game called [http://en.wikipedia.org/wiki/Gravity_Wars 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.  
 
The goal of this tutorial is to make a simple game. Years ago Amiga had a game called [http://en.wikipedia.org/wiki/Gravity_Wars 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 ===
 
=== Contents ===
 
This tutorial is divided into several small parts
 
This tutorial is divided into several small parts
* In [[GLBasic Tutorial Part 1|part 1]] we first make a plan of the game and then create planets.
+
* In [[GLBasic_tutorial#First_we_need_a_plan|part 1]] we first make a plan of the game and then create planets.
* In [[GLBasic Tutorial Part 2|part 2]] we shoot some torpedoes.
+
* In [[GLBasic_tutorial#A_new_type|part 2]] we shoot some torpedoes.
* [[GLBasic Tutorial Part 3|Part 3]] adds sprites and lets us aim those torpedoes.
+
* [[GLBasic_tutorial#Sprites|Part 3]] adds sprites and lets us aim those torpedoes.
* [[GLBasic Tutorial Part 4|Part 4]] adds gravity and nicer planets.
+
* [[GLBasic_tutorial#Creating_better_planets|Part 4]] adds gravity and nicer planets.
* [[GLBasic Tutorial Part 5|Part 5]] will introduce an opponent and a more scalable way to process input.
+
* [[GLBasic_tutorial#Adding_an_opponent|Part 5]] will introduce an opponent and a more scalable way to process input.
* Parts [[GLBasic Tutorial Part 6|6]], [[GLBasic Tutorial Part 7|7]], etc. will keep improving our game, but I haven't yet planned that far...
+
* [[GLBasic_tutorial#Making_the_game_playable|Part 6]] will finally make the game actually playable.
 +
* [[GLBasic_tutorial#Compile_for_Pandora|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:
 +
 
 +
[[Image:GLBasic_Tutorial_5.jpg]]
 +
 
 +
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) [http://www.glbasic.com/xmlhelp.php?lang=en&id=0&action=view 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 [http://www.glbasic.com/forum/index.php?topic=4777.0 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 ===
 +
<pre>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
 +
</pre>
 +
 
 +
A type declaration always starts with word <code>TYPE</code> and the name of the type which in this case is <code>TPlanet</code>. 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 <code>TPlanet</code> instead of just <code>Planet</code>.
 +
 
 +
<code>TPlanet</code> 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
 +
 
 +
<pre>TYPE TPlanet
 +
x
 +
y
 +
radius%
 +
density%
 +
mass%
 +
</pre>
 +
These are the variables that describe the attributes of the planet. <code>x</code> and <code>y</code> are floating point numbers, that is they can have decimals in them. Variables that have a <code>%</code> after the name are integer variables and don't have decimals.
 +
 
 +
<pre> // creates a new random sized planet in random coordinates
 +
// returns: always 0
 +
</pre>
 +
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.
 +
 
 +
<pre> FUNCTION Create:
 +
</pre>
 +
A function always starts with keyword <code>FUNCTION</code> followed by the name of the function and a colon.
 +
 
 +
<pre> self.x = RND(600)+100.0
 +
self.y = RND(380)+50.0
 +
</pre>
 +
These two lines initialize the location of the planet. The keyword <code>self</code> refers to the type object itself, that is <code>self.x</code> means the variable <code>x</code> of this object. [http://www.glbasic.com/xmlhelp.php?lang=en&id=71&action=view 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.
 +
 
 +
<pre> // density is 1-4
 +
self.density = RND(3)+1
 +
self.radius = RND(60)+20
 +
</pre>
 +
Next is another comment and two more random variable initializations.
 +
 
 +
<pre> // 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
 +
</pre>
 +
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.
 +
 
 +
<pre> // 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
 +
</pre>
 +
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.
 +
 
 +
<pre>//////////////////////////////////
 +
//// Main program starts here ////
 +
//////////////////////////////////
 +
 
 +
LOCAL planets[] AS TPlanet
 +
DIM planets[7]
 +
</pre>
 +
First there are three comment lines. Then an array called <code>planets</code> 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.
 +
<pre>SETSCREEN 800,480,0
 +
// create and draw the planets
 +
CreatePlanets(planets[])
 +
</pre>
 +
What was that previous line? That was a function call which passed the recently declared array to the function as a parameter.
 +
<pre>// display the created planets
 +
SHOWSCREEN
 +
</pre>
 +
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.
 +
<pre>// wait for the user to click a mousebutton
 +
MOUSEWAIT
 +
// exit program
 +
END
 +
////////////////////////////////
 +
//// Main program ends here ////
 +
////////////////////////////////
 +
</pre>
 +
 
 +
=== 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 <code>aPlanets</code> used in the argument list
 +
 
 +
<pre>// 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
 +
</pre>
 +
First the function needs to declare variables before it can start doing things.
 +
<pre> FOR i=0 TO LEN(aPlanets)-1
 +
</pre>
 +
This line defines a for-loop that start at value zero and ends at <code>LEN(aPlanets)-1</code>. <code>LEN(aPlanets)</code> returns the number of items in its parameter array. Since <code>aPlanets</code> is the parameter of this function and this function is called with the <code>planets</code>-array as a parameter, it returns 7 in this case. So, the loop could have been written as <code>FOR i=0 TO 6</code> 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 <code>i</code> 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.
 +
 
 +
<code>WHILE ok=false</code> starts another loop which ends with the keyword <code>WEND</code>. The difference is that there is no loop-variable which gets sequential values. Instead there is a condition which defines how many times <code>WHILE</code>-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 <code>ok</code> 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 [http://www.glbasic.com/xmlhelp.php?lang=en&id=173&action=view BREAK]-statement.
 +
 
 +
Notice how the innermost for-loop uses the variable defined in the outermost loop:
 +
<pre>FOR j=0 TO i-1
 +
</pre>
 +
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 ==
 +
<pre>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
 +
</pre>
 +
== And here's the result ==
 +
[[Image:GLBasic_Tutorial_1.png]]
 +
 
 +
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 [[GLBasic Tutorial Part 2|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
 +
<pre>TYPE TTorpedo
 +
// current location
 +
curX
 +
curY
 +
// previous location
 +
prevX
 +
prevY
 +
// movement along each axis
 +
dx
 +
dy
 +
</pre>
 +
Next is a function which launches a torpedo. It gets the starting location, direction and speed as arguments.
 +
<pre> // 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
 +
</pre>
 +
The following function moves the torpedo and draws it on the screen.
 +
<pre> // 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
 +
</pre>
 +
This last function checks if the torpedo has collided with any of the planets in the argument array.
 +
<pre> // 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
 +
</pre>
 +
== 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.
 +
<pre>//////////////////////////////////
 +
//// 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 ////
 +
////////////////////////////////
 +
</pre>
 +
=== Running the new program ===
 +
The result isn't quite what was expected.
 +
 
 +
[[Image:GLBasic_Tutorial_2.png]]
 +
 
 +
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
 +
 
 +
<pre>// 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
 +
</pre>
 +
 
 +
[[Image:GLBasic_Tutorial_2b.png]]
 +
 
 +
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.
 +
<pre>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
 +
</pre>
 +
 
 +
=== 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 [[GLBasic Tutorial Part 3|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 [http://www.google.fi/images?q=spaceship+sprites Google's image search] to find some nice lookings spaceships.
 +
I decided to use some drawn by Kryptid which are available [http://kryptid.deviantart.com/art/Spaceship-Sprite-Package-117026929 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.
 +
 
 +
[[Image:GLBASIC_Ship1.png]][[Image:GLBASIC_Ship2.png]]
 +
 
 +
You could also use BMPs as sprites, but then you have to specify the transparent color with the <code>SETTRANSPARENCY</code>-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 ===
 +
<pre>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
 +
</pre>
 +
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.
 +
<pre> 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
 +
</pre>
 +
The following two functions handle the input from the player. The longer the player keeps turning in one direction, the faster the ship turns.
 +
<pre> 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
 +
</pre>
 +
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.
 +
<pre> 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
 +
</pre>
 +
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.
 +
<pre> 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
 +
</pre>
 +
 
 +
=== Changes to the TTorpedo-type ===
 +
 
 +
The TTorpedo-type has been changed a bit. I added an indicator <code>isMoving</code> which tells if whether the torpedo has been launced and should
 +
be drawn or not. Functions <code>Launch</code> and <code>Move</code> have been changed accordingly.
 +
 
 +
<pre> // 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
 +
</pre>
 +
=== 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.
 +
<pre>//////////////////////////////////
 +
//// 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 ////
 +
////////////////////////////////
 +
</pre>
 +
You can try turning the ship with arrow keys and shooting torpedoes with return.
 +
 
 +
=== Complete code so far ===
 +
<pre>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
 +
</pre>
 +
=== What's next ===
 +
In [[GLBasic Tutorial Part 4|part 4]] will make the planets prettier and add gravity. You can also return to [[GLBasic Tutorial Part 2|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
 +
[http://www.ironstarmedia.co.uk/blog/2009/12/free-game-assets-05-planet-sprites-and-textures/ 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.
 +
 
 +
[[Image:GLBASIC_Planet1.png]][[Image:GLBASIC_Planet2.png]][[Image:GLBASIC_Planet3.png]][[Image:GLBASIC_Planet4.png]]
 +
 
 +
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 <code>LOADBMP</code> 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.
 +
<pre>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
 +
</pre>
 +
The loaded sprites are then used <code>Draw</code>-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.
 +
<pre> 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
 +
</pre>
 +
Now the game looks much better!
 +
 
 +
[[Image:GLBasic_Tutorial_4.jpg]]
 +
 
 +
=== Gravity ===
 +
 
 +
Next we'll add gravity to the game. It's done in the TTorpedo-type's function [http://www.google.fi/images?q=%2Bobey+%2Bgravity+t-shirt ObeyGravity] which looks like this:
 +
<pre> // 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
 +
</pre>
 +
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.
 +
 
 +
<pre>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
 +
</pre>
 +
When you try shooting torpedoes now, the planets' gravitational pull affect the flight path.
 +
 
 +
=== Complete code ===
 +
 
 +
<pre>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
 +
</pre>
 +
=== What's next? ===
 +
 
 +
In the [[GLBasic_Tutorial_Part_5|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.
 +
<pre>
 +
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)
 +
</pre>
 +
 
 +
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:
 +
 
 +
<pre>// 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
 +
</pre>
 +
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.
 +
 
 +
<pre> // 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
 +
</pre>
 +
The new variables are used in the CheckInput-function.
 +
<pre> 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
 +
</pre>
 +
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...
 +
 
 +
[[Image:GLBasic_Tutorial_5.jpg]]
 +
 
 +
=== Complete code ===
 +
<pre>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
 +
</pre>
 +
 
 +
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 [http://git.openpandora.org/cgi-bin/gitweb.cgi?p=pandora-libraries.git;a=blob;f=docs/examples/average-case_PXML.xml 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 [http://boards.openpandora.org/index.php?/topic/3756-pndtools/ 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.
 +
 
  
Keep in mind the game has not been written yet when I've started writing this tutorial. Hopefully it will be finished some day.
+
[[Category:Tutorials]]
 +
[[Category:GLBasic]]

Latest revision as of 11:59, 17 August 2013

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 5.jpg

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

GLBasic Tutorial 1.png

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.

GLBasic Tutorial 2.png

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

GLBasic Tutorial 2b.png

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.

GLBASIC Ship1.pngGLBASIC Ship2.png

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.

GLBASIC Planet1.pngGLBASIC Planet2.pngGLBASIC Planet3.pngGLBASIC Planet4.png

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!

GLBasic Tutorial 4.jpg

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...

GLBasic Tutorial 5.jpg

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.