Angles in 2D environment and artificial smarts based on them

Written by Lachie Dazdarian (July, 2007)

Introduction

I'm this dummy level tutorial I'll try to teach you how to retrieve an angle between two objects in a 2D environment, and then use this angle to create fun artificial smarts which can, for example, follow around the screen a circle you control. All this can on another level be used in various top-down games that feature 360 degrees rotation to tell enemy CPU controlled objects to react on the player on various ways (attack, run away, hit & run, etc.).

You could describe this as a more smart way of telling one object to follow another than using x and y pixel distances. The method with x and y distances can be sufficient to some extent, but you'll most likely find it crude and limited.

To extend the scope of the tutorial, in the second part of it I'll add to player the ability to shoot projectiles and destroy the circles that follow him. This will illustrate how projectiles shot with an angle should be managed, and we'll repeat some stuff from my "How To Program A Game With FreeBASIC - Lesson #2" tutorial.

The code was written in FreeBASIC ver.0.18 and compiles with –lang fb (should be fully compatible with FreeBASIC ver.0.17).

Part One

We’ll work in the Cartesian coordinate system, system used to determine each point uniquely in a plane through two numbers, usually called the x-coordinate and the y-coordinate of the point. This is a given coordinate system with most programming languages as it results from the nature of computer monitors (stop laughing). For any other coordinate system, like polar, we would have to create it first from the Cartesian coordinate system. I still didn’t work in any other coordinate system so can’t tell you in what kind of 2D games they can be useful.

Some basic trigonometry knowledge will help a lot in understanding this tutorial.

Let's first start by declaring variables we'll need in our example program.

' We include FreeBASIC's built-in library and allow
' the usage of its additional constants and functions
' with Using FB.
#include "fbgfx.bi"
Using FB

' We set some needed constants.
const fpslimit = 60
const FALSE = 0
const TRUE = 1
const PI = 3.141593

' We declare the needed variables.
DIM SHARED workpage AS INTEGER

' The following 3 variables are used for
' frames per second control.
DIM SHARED st AS DOUBLE
DIM SHARED frameintvl As Double = 1.0/fpslimit
DIM SHARED sleepintvl As Integer

DIM SHARED angledeg AS INTEGER  ' Main object's angle in degrees
DIM SHARED anglerad AS SINGLE   ' Main object's angle in radians
DIM SHARED mainx AS SINGLE      ' Main object's x pos
DIM SHARED mainy AS SINGLE      ' Main object's y pos
DIM SHARED mainspeed AS SINGLE      ' Main object's speed
DIM SHARED main_rotationspeed AS SINGLE ' Main object's rotation speed
DIM SHARED resultangledeg AS INTEGER ' Angle between the cpu cont. object and main object in degrees
DIM SHARED resultanglerad AS SINGLE  ' Angle between the cpu cont. object and main object in radians
DIM SHARED CPUobjectx AS SINGLE    ' CPU controlled object's x pos
DIM SHARED CPUobjecty AS SINGLE    ' CPU controlled object's y pos
DIM SHARED CPUobj_angledeg AS INTEGER ' CPU controlled object's angle in degrees
DIM SHARED CPUobj_anglerad AS SINGLE  ' CPU controlled object's angle in radians
DIM SHARED CPUobject_rotationspeed AS SINGLE ' Rotation speed of the CPU controlled object
DIM SHARED CPUobjectspeed AS SINGLE ' CPU controlled object's speed
DIM SHARED ASmode AS INTEGER ' artificial smarts control (on/off)

I think the comments explain most of it, with FPS control variables and the FB’s built-in graphics library initiation not belonging to the topic of this tutorial, but are something you should take a note of. It's quite obvious we'll need variables to store x and y positions of our two objects, their current angles in degrees (0 to 360) and radians (0 to 2*PI), their speeds, variables for the result angle (angle between the two objects), and a help variable that allows us to turn on/off the artificial smarts. Objects' positions and angles in radians should be declared with SINGLE or DOUBLE precision.

Constants for FALSE and TRUE you should declare always, and the PI constant too when using angles in your project.

After declaring all the variables we need, we should initiate our screen and variables' initial values.

' We set our screen to 640 width, 480 height, 32 color bit-depth,
' 2 work pages and full screen.
SCREENRES 640, 480, 32, 2, GFX_FULLSCREEN

' We set the initial values for the main object's
' and CPU controlled object's positions/angles/speeds.
mainx = 320
mainy = 220
mainspeed = 4
main_rotationspeed = 3
angledeg = 0
CPUobjectx = 200
CPUobjecty = 200
CPUobj_angledeg = 20
CPUobj_anglerad = (CPUobj_angledeg*PI)/180
CPUobject_rotationspeed = 3
CPUobjectspeed = 2
ASMode = TRUE

Everything clear here. Note how computer object's angle is converted from degrees to radians. We need this value in radians because radians are used with the FreeBASIC's SIN and COS functions (later in the tutorial).

From Wikipedia:

One radian is the angle subtended at the center of a circle by an arc of circumference that is equal in length to the radius of the circle. It equals to 180/PI degrees, or about 57.2958 degrees. In calculus and most other branches of mathematics beyond practical geometry, angles are universally measured in radians. One important reason is that results involving trigonometric functions are simple and "natural" when the function's argument is expressed in radians.



In most mathematical work beyond practical geometry, angles are typically measured in radians rather than degrees. This is for a variety of reasons; for example, the trigonometric functions have simpler and more "natural" properties when their arguments are expressed in radians. These considerations outweigh the convenient divisibility of the number 360. One complete circle (360°) is equal to 2*PI radians, so 180° is equal to PI radians, or equivalently, the degree is a mathematical constant ° = PI/180.




Now let's create a standard DO...LOOP where our program will be happening.

' Your average do loop.
DO
    
st = Timer ' Record the current time into st variable
           ' (used to limit FPS; no related to the
           ' topic of the example program).
  
screenlock ' Lock our screen (nothing will be
           ' displayed until we unlock the screen).
screenset workpage, workpage xor 1 ' Swap work pages.

CLS ' Clear the screen

workpage xor = 1 ' Swap work pages.
screenunlock ' Unlock the page to display what has been drawn.

' Keep the FPS value within fpslimit (set above).
sleepintvl = Cint((st + frameintvl - Timer)*1000.0)
If sleepintvl>1 Then
  Sleep sleepintvl
end if

LOOP UNTIL MULTIKEY(SC_ESCAPE) ' Do loop until ESC is pressed.

Let's now draw few circles that will represent our objects, and lines that will represent their direction. Put this above workpage xor 1.

' We draw our objects (represented with small circles) and
' lines that emulate these objects' directions.
LINE (CPUobjectx, CPUobjecty)-(CPUobjectx+sin(CPUobj_anglerad)*20,CPUobjecty-cos(CPUobj_anglerad)*20), RGB(2,117, 250)
CIRCLE (CPUobjectx, CPUobjecty), 3, RGB(2,117, 250)
LINE (mainx, mainy)-(mainx+sin(anglerad)*20,mainy-cos(anglerad)*20), RGB(200, 0, 0)
CIRCLE (mainx, mainy), 3, RGB(200, 0, 0)

Why adding SIN * object's_angle * 20 to X coordinate?

Allow me to post few pictures now that will help us to clear few things up.


Picture 1



Picture 2


If you refer to picture 1 and 2 above, you'll note that a in our system (system where 0 degrees is a vertical line going from bottom to top) is delta x (x2 - x1), and a is c * sin alfa, c representing the length of our line (or shortest distance from the point of origin - center of the object in our case). With y the situation is the same, only we use COS and we need to deduct it because in FreeBASIC the positive direction of the y axis goes from top to bottom. So COS * object's_angle in our system equals -(COS * object's_angle) in the default BASIC coordinate system. You can choose to work in the default BASIC coordinate system, but then your 0 degrees will be rotated by +PI (where 180 degrees is on the picture above). Anyway, I chose to work with the standard axis directions because they are burned into my brain from school. I find it less frustrating to make these corrections than accepting the inverted direction of the y axis.

Before all, we need to find a way how to retrieve the angle between our two objects. We have their coordinates, x and y, and from these coordinates we can calculate the distances between the two objects vertically and horizontally. To get the angle we'll use the arctangent function (ATAN2 (delta y, delta x) in FreeBASIC) which retrieves an angle from objects' distances vertically over objects' distances horizontally (delta y / delta x). Use the following lines to get alfa 2 (picture 1). Put this in the DO...LOOP before CLS:

resultanglerad = ATAN2((-1)*(mainy-CPUobjecty),(mainx-CPUobjectx))
resultanglerad = PI/2 - resultanglerad
IF resultanglerad < 0 THEN resultanglerad = resultanglerad + 2*PI 

Think of CPUobjectx, CPUObjecty as of x1, y1 and mainx, mainy as of x2, y2. We need to multiply delta y with -1 because of the mentioned difference in the direction of the y axis, and since we need alfa 1 and not alfa 2 we deduct alfa 2 from PI/2 (90 degrees) to get alfa 1. Check picture 1 again. The third line in the code keeps our angle within 0 to 2*PI scope (full circle), to keep things clear (negative angles confuse)

That's one way of doing it. To get alfa 1 directly, we can to this:

resultanglerad = ATAN2((mainx-CPUobjectx), (CPUobjecty-mainy))
IF resultanglerad < 0 THEN resultanglerad = resultanglerad + 2*PI 

This code does the compensation for the difference in the direction of the y axis, and for the fact our angle starts vertically from bottom to top (we replaced the positions of delta x and delta y in the function).

What ever you chose, it will work. Using the ATN function is more cumbersome and requires a more complex formula to retrieve the angle properly. Thus, I'm using ATAN2.

We also need to convert the calculated angle from radians to degrees so we can use its value in degrees with the artificial smart algorithm (it simplifies the code):

resultangledeg = (resultanglerad*180)/PI

After we found a way to retrieve the angle between our two objects, let's add some control for our player. We'll use left/right arrow key for angle change (rotation), and up arrow key for thrust.

' The control keys for the main (player's) object.
IF MULTIKEY(SC_LEFT) THEN angledeg = angledeg - main_rotationspeed
IF MULTIKEY(SC_RIGHT) THEN angledeg = angledeg + main_rotationspeed
' The following lines keep the angle between
' 0 and 360.
IF angledeg<0 THEN angledeg=angledeg+360
IF angledeg>359 THEN angledeg=angledeg-360
anglerad = (angledeg*PI)/180 ' Convert the angle from degrees to radians
IF MULTIKEY(SC_UP) THEN 
mainx = mainx + sin(anglerad)*mainspeed
mainy = mainy - cos(anglerad)*mainspeed
END IF

' For turning AS on/off.
IF MULTIKEY(SC_1) THEN ASMode = TRUE
IF MULTIKEY(SC_2) THEN ASMode = FALSE

Note how the position of the main object is changed when the user presses the up arrow key. In each loop the object's x position is changed by the SIN of its current angle times its speed (picture 1 again), while the y position is changed by the COS of its current angle times its speed (deduced for the reasons explained above).

The last two statements are for turning on/off the artifical smart.

The entire source so far: codever1.txt

If you compile this you'll be able to move your object (red color), but the sky blue computer controlled object will just sit there since we didn't implemented any artificial smart algorithm.

So we need to tell our computer controlled object to rotate toward the player and move constantly. No problem! First, let's solve the rotation. Rotation will be handled like this. First, we have the angle between the computer controlled object and the player, and the current angle of the computer controlled object. By comparing these two angles and checking which rotation (clockwise or counter-clockwise) is of shorter distance for computer controlled object's angle to equalize with the result angle (angle between the two objects), we'll say to our computer controlled object to rotate in one or the other direction. Just observe the following code:

' If ASMode is true apply the artificial smart code on the CPU controlled object
IF ASMode = TRUE THEN
    ' If the CPU controled object's angle is larger than the
    ' result angle (angle between the CPU object and player's object)...
    IF CPUobj_angledeg > resultangledeg THEN
        ' If the difference between the current angle of the
        ' CPU controlled object and the result angle going
        ' counter-clockwise is less than this difference
        ' clockwise, rotate the CPU object counter-clockwise
        IF (360-CPUobj_angledeg+resultangledeg) >= (CPUobj_angledeg-resultangledeg) THEN CPUobj_angledeg = CPUobj_angledeg - CPUobject_rotationspeed
        ' If the difference between the current angle of the
        ' CPU controlled object and the result angle going
        ' clockwise is less that this difference counter-clockwise,
        ' rotate the CPU object clockwise
        IF (360-CPUobj_angledeg+resultangledeg) < (CPUobj_angledeg-resultangledeg) THEN CPUobj_angledeg = CPUobj_angledeg + CPUobject_rotationspeed
    END IF
    ' Same as above but for situation when CPU object's angle is
    ' less that the result angle.
    IF CPUobj_angledeg < resultangledeg THEN
        IF (360-resultangledeg+CPUobj_angledeg) >= (resultangledeg-CPUobj_angledeg) THEN CPUobj_angledeg = CPUobj_angledeg + CPUobject_rotationspeed
        IF (360-resultangledeg+CPUobj_angledeg) < (resultangledeg-CPUobj_angledeg) THEN CPUobj_angledeg = CPUobj_angledeg - CPUobject_rotationspeed
    END IF
    ' The following lines keep the CPU object's angle within
    ' 0 to 360 degrees area.
    IF CPUobj_angledeg<0 THEN CPUobj_angledeg=CPUobj_angledeg+360
    IF CPUobj_angledeg>359 THEN CPUobj_angledeg=CPUobj_angledeg-360
    ' Convert the CPU object's angle from degrees to radians.
    CPUobj_anglerad = (CPUobj_angledeg*PI)/180
    ' Move the CPU object according to angle and speed
    ' (note how SIN and COS functions are used).
    ' Since positive y axis goes down in FB, we need to reduce
    ' the y position of the object by the COS function and not add 
    ' it as in normal Cartesian coordinate system.
    CPUobjectx = CPUobjectx + sin(CPUobj_anglerad)*CPUobjectspeed
    CPUobjecty = CPUobjecty - cos(CPUobj_anglerad)*CPUobjectspeed
END IF

The formulas for calculating the shorter distance to reach resultangle from one or the other direction might confuse you, but they are really simple. For example, let's say computer controlled object's angle is 275 ° and the angle between the two objects is 25 °.

First condition...

275 > 25 --> CPUobj_angledeg > resultangledeg

It's obvious that...

360 - 275 + 25 --> 360 - CPUobj_angledeg + resultangledeg (distance clockwise from CPUobj_angledeg to resultangle)

...is less than...

275 - 25 --> CPUobj_angledeg - resultangledeg (distance counter-clockwise from CPUobj_angledeg to resultangle)

...so we need to increase the angle (clockwise rotation) until it reaches 25 °. When the angle exceeds 360 ° during this increasing, it jumps back to 360 - CPUobj_angledeg (which is above 360 °).

Movement is done like with the player, but in this example we'll tell the computer controlled object to move constantly. Since the computer controlled object will be always rotating toward the player, the effect will be satisfactory. Perhaps in the second edition of this tutorial I can show you how to use different AS modes for one object so it wouldn't behave the same way ALL the time.

ASMode variable is used to turn on/off the movement of the computer controlled object and it only serves a purpose in this example program. Still, this kind of variable might come in handy during development of a game, allowing you to position player's object on any location near the computer controlled object in order to test its behavior in various situations.

Put the last piece of code after the resultangle calculation, compile it, and test it. Neat, eh?

For the very end of the first part of this tutorial, add the following lines in the code after CLS to make it all look more informative:

' We draw an help Cartesian coordinate system.
LINE (50, 80)-(110,80), RGB(255,255,255)
LINE (80, 50)-(80,110), RGB(255,255,255)
Draw String (77,40), "0", RGB(0, 176, 214)
Draw String (69,114), "180", RGB(0, 176, 214)
Draw String (114,76), "90", RGB(0, 176, 214)
Draw String (23,76), "270", RGB(0, 176, 214)

Draw String (12,200), "1 - AI On", RGB(0, 176, 214)
Draw String (12,210), "2 - AI Off", RGB(0, 176, 214)

' We display some useful textual information.
Draw String (320,10), "Main object's angle:"+STR$(angledeg), RGB(2,117, 190)
Draw String (320,20), "Computer controlled object's angle:"+STR$(CPUobj_angledeg), RGB(2,117, 190)
Draw String (320,30), "Angle between objects:"+STR$(resultangledeg), RGB(2,117, 190)

The entire code so far: codever2.txt

Screenshot of the result:

Compile the last source and enjoy. You can play with the rotation speed of the computer controlled object if curious.

Now let's move on.

Part Two

In the second part of this tutorial we'll add multiple objects and ability to player to shoot projectiles. Buckle up!

As I did in my second edition of "How To Program A Game With FreeBASIC" (QB Express #20), we'll declare computer controlled objects as an array, with each element in the array representing one single object. With each program start, computer controlled objects' positions will be randomized.

For this section, just read the tutorial. The old code will be tumbled up and down a lot, so it's best for you compile the entire source when I provide it later.

First, we will change the way we declare computer controlled objects. We'll use a custom defined type (check my other tutorial, previously mentioned).

After constants declarations we need to have this code:

TYPE ObjType
X             AS SINGLE   ' Used to flag object's x position.
Y             AS SINGLE   ' Used to flag object's y position.
AngleDeg      AS INTEGER  ' Used to flag object's angle in degrees.
AngleRad      AS SINGLE   ' Used to flag object's angle in radians.
Speed         AS SINGLE   ' Used to flag object's speed.
RotationSpeed AS SINGLE   ' Used to flag object's rotation speed
Active        AS INTEGER  ' Used to flag object's status
ActiveTime    AS INTEGER  ' Use to expire object's activity (once we activate it).
Typ    AS INTEGER         ' Used to flag type of the object (if we want to
                          ' have more kinds of the same object -> different
                          ' ships, projectiles, etc.).
END TYPE

It contains all variables our computer controlled objects need, and what later projectiles will need to be managed.

After this we need to declare our computer controlled objects with:

DIM SHARED CPUobject(numofCPUobjects) AS ObjType

numofCPUobjects needs to be declared before as a variable or a constant. numofCPUobjects will represent the maximum number of computer controlled objects that can be active in a single moment, and it's good to declare it as a constant or a variable so we can change our program parameters easily. I've set it to 20.

We must initiate the positions of the computer controlled objects and their speeds like we did before with a single computer controlled object. This is done with the following code:

' Get the random seed from the seconds past midnight (the
' best way to get random numbers).
RANDOMIZE TIMER

FOR initCPUobj AS INTEGER = 1 TO numofCPUobjects
    CPUobject(initCPUobj).X = INT(RND * 600) + 20 ' Randomize cpu object's position from 20 to 620
    CPUobject(initCPUobj).Y = INT(RND * 440) + 20 ' Randomize cpu object's position from 20 to 460
    CPUobject(initCPUobj).AngleDeg = INT(RND * 360) + 1 ' Randomize cpu object's angle from 1 to 360
    CPUobject(initCPUobj).AngleRad = (CPUobject(initCPUobj).AngleDeg*PI)/180
    CPUobject(initCPUobj).RotationSpeed = INT(RND * 2) + 2 ' Randomize cpu object's rotation speed from 2 to 3
    CPUobject(initCPUobj).Speed = INT(RND * 3) + 1 ' Randomize cpu object's rotation speed from 1 to 3
    CPUobject(initCPUobj).Active = TRUE ' All object active (alive) by default.
NEXT initCPUobj

We loop hrough our objects and randomize their positions, regular and rotation speeds (to get a better effect since all objects won't move and rotate with the same speeds).

The whole phylosophy after this is in altering the artificial smart code and computer controlled objects' drawing code so if would draw and move all the computer controlled objects.

Observe the following code:

FOR countCPUobj AS INTEGER = 1 TO numofCPUobjects ' Loop through all the objects
    
    ' If ASMode is true apply the artificial smart code on the current CPU controlled object
    IF ASMode = TRUE THEN
        
        ' The following lines calculate the angle of direction
        ' from the current controlled object toward the main object.
        resultanglerad = ATAN2((-1)*(mainy-CPUobject(countCPUobj).Y),(mainx-CPUobject(countCPUobj).X))
        resultanglerad = PI/2 - resultanglerad
        IF resultanglerad < 0 THEN resultanglerad = resultanglerad + 2*PI 
        
        ' The following line converts the result angle between the
        ' two objects from radians to degrees.
        resultangledeg = (resultanglerad*180)/PI
        
        ' If the CPU controled object's angle is larger than the
        ' result angle (angle between the CPU object and player's object)...
        IF CPUobject(countCPUobj).AngleDeg > resultangledeg THEN
            ' If the difference between the current angle of the
            ' CPU controlled object and the result angle going
            ' counter-clockwise is less than this difference
            ' clockwise, rotate the CPU object counter-clockwise
            IF (360-CPUobject(countCPUobj).AngleDeg+resultangledeg) >= (CPUobject(countCPUobj).AngleDeg-resultangledeg) THEN CPUobject(countCPUobj).AngleDeg = CPUobject(countCPUobj).AngleDeg - CPUobject(countCPUobj).RotationSpeed
            ' If the difference between the current angle of the
            ' CPU controlled object and the result angle going
            ' clockwise is less that this difference counter-clockwise,
            ' rotate the CPU object clockwise
            IF (360-CPUobject(countCPUobj).AngleDeg+resultangledeg) < (CPUobject(countCPUobj).AngleDeg-resultangledeg) THEN CPUobject(countCPUobj).AngleDeg = CPUobject(countCPUobj).AngleDeg + CPUobject(countCPUobj).RotationSpeed
        END IF
        ' Same as above but for situation when CPU object's angle is
        ' less that the result angle.
        IF CPUobject(countCPUobj).AngleDeg < resultangledeg THEN
            IF (360-resultangledeg+CPUobject(countCPUobj).AngleDeg) >= (resultangledeg-CPUobject(countCPUobj).AngleDeg) THEN CPUobject(countCPUobj).AngleDeg = CPUobject(countCPUobj).AngleDeg + CPUobject(countCPUobj).RotationSpeed
            IF (360-resultangledeg+CPUobject(countCPUobj).AngleDeg) < (resultangledeg-CPUobject(countCPUobj).AngleDeg) THEN CPUobject(countCPUobj).AngleDeg = CPUobject(countCPUobj).AngleDeg - CPUobject(countCPUobj).RotationSpeed
        END IF
        ' The following lines keep the current CPU object's angle within
        ' 0 to 360 degrees area.
        IF CPUobject(countCPUobj).AngleDeg<0 THEN CPUobject(countCPUobj).AngleDeg=CPUobject(countCPUobj).AngleDeg+360
        IF CPUobject(countCPUobj).AngleDeg>359 THEN CPUobject(countCPUobj).AngleDeg=CPUobject(countCPUobj).AngleDeg-360
        ' Convert the CPU object's angle from degrees to radians.
        CPUobject(countCPUobj).AngleRad = (CPUobject(countCPUobj).AngleDeg*PI)/180
        ' Move the current CPU object according to angle and speed
        ' (note how SIN and COS functions are used).
        ' Since positive y axis goes down in FB, we need to reduce
        ' the y position of the object by the COS function and not add 
        ' it as in normal Cartesian coordinate system.
        CPUobject(countCPUobj).X = CPUobject(countCPUobj).X + sin(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed
        CPUobject(countCPUobj).Y = CPUobject(countCPUobj).Y - cos(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed
        
    END IF

    ' We draw the current computer controlled object if active (alive).
    IF CPUobject(countCPUobj).Active = TRUE THEN
        LINE (CPUobject(countCPUobj).X, CPUobject(countCPUobj).Y)-(CPUobject(countCPUobj).X+sin(CPUobject(countCPUobj).AngleRad)*20,CPUobject(countCPUobj).Y-cos(CPUobject(countCPUobj).AngleRad)*20), RGB(2,117, 250)
        CIRCLE (CPUobject(countCPUobj).X, CPUobject(countCPUobj).Y), 3, RGB(2,117, 250)
    END IF
    
NEXT countCPUobj

The code is identical to that where a single computer controlled object is present, only now, like with variable initiation, we loop through all the computer controlled objects, check their angle with the main object, and rotate them accordingly. Drawing of the computer controlled objects also had to be placed in the FOR loop.

The entire code so far: codever3.txt

Few things are left to be done. First, let's implement projectiles. This was covered in my other tutorial (QB Expresse #20), but no harm in repeating some of the stuff.

For contolling projectiles we'll use ObjType custom defined type.

Projectiles will be declared with:

DIM SHARED Projectile(numofprojectiles) AS ObjType

Where numofprojectiles will be declared earlier as a constant or a variable, representing the maximum number of projectile that can be active simultaneously. I've set it to 50.

We should now construct a subroutine that will initiate a new projectile. I named it InitiateProjectile and constructed it like this:

SUB InitiateProjectile (px AS SINGLE, py AS SINGLE, pangle AS SINGLE, ptyp AS INTEGER)

' We loop through our projectiles looking for a free one (never used
' before or expired -> ActiveTime = 0).
FOR initproj AS INTEGER = 1 TO numofprojectiles
    
    ' When a free projectile is found we set its position, type,
    ' life time (ActiveTime = 30 -> 30 loops) and angle. After this
    ' the subroutine is exited so that a next free projectile wouldn't
    ' be initiated too.
    IF Projectile(initproj).ActiveTime = 0 THEN
        Projectile(initproj).X = px
        Projectile(initproj).Y = py
        Projectile(initproj).AngleRad = pangle
        Projectile(initproj).Typ = ptyp
        ' According to given projectile's type we can
        ' set other projectile's characteristics.
        ' In this program we'll only set projectile's
        ' speed according to its type.
        
        ' Projectile type 1 will and should only be used
        ' with the main player since we added a condition
        ' that the projectile's speed will be increased
        ' by the main object's speed if it is moving.
        IF Projectile(initproj).Typ = 1 THEN 
            Projectile(initproj).Speed = 5
            IF MULTIKEY(SC_UP) THEN Projectile(initproj).Speed =  Projectile(initproj).Speed + mainspeed
        END IF
        Projectile(initproj).ActiveTime = 80
        EXIT SUB
    END IF
    
NEXT initproj
 
END SUB

If you read my previous tutorials this should be clear. An projectile is initiated with:


InitiateProjectile start_x_position, start_y_position, projectiles_angle, projectile_type

...with the sections shown filled properly.

Inside the sub program goes through the projectiles, looks for an inactive one (ActiveTime = 0). When a "free" projectle is found, its values are set according to values inputted when the sub is called. After this the sub is exited so that another free projectile wouldn't be initiated in the same manner.

This sub should be declared after constants declarations with:


DECLARE SUB InitiateProjectile (px AS SINGLE, py AS SINGLE, pangle AS SINGLE, ptyp AS INTEGER)

The second sub we need is the one that moves and draws activated projectiles. Just observe the following code:

SUB DrawProjectiles ()
    
FOR countproj AS INTEGER = 1 TO numofprojectiles
    
    ' We loop through our projectiles and if an active one is
    ' found, we move and draw it.
    IF Projectile(countproj).ActiveTime > 0 THEN
    
        ' The next line is used to expire the projectile so it wouldn't
        ' be active infinitely. We can do this on different ways, like
        ' by deactivating an object once it passes the edge of screen.
        ' Still, this is a very handy way of setting the "life time" of an object.
        Projectile(countproj).ActiveTime = Projectile(countproj).ActiveTime - 1
        
        ' Projectiles are moved just like the main and computer controlled
        ' objects.
        Projectile(countproj).X = Projectile(countproj).X + sin(Projectile(countproj).AngleRad)*Projectile(countproj).Speed
        Projectile(countproj).Y = Projectile(countproj).Y - cos(Projectile(countproj).AngleRad)*Projectile(countproj).Speed
        
        ' According to projectile type, we draw it.
        IF Projectile(countproj).Typ = 1 THEN
            LINE (Projectile(countproj).X, Projectile(countproj).Y)-(Projectile(countproj).X+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cos(Projectile(countproj).AngleRad)*3), RGB(192, 192, 0)
        END IF
        
    END IF
    
NEXT countproj
    
END SUB

The comments explain most of it. Only active objects are managed (ActiveTime > 0), the movement is done like with the main and computer controlled objects, and drawing is done according to projectile's type. Since we only have one type of projectiles in our example program, the use of that IF clause doesn't seem much logical, but will most likely be in a proper game where more kinds of projectiles (weapons) are used.

Above the last "END IF", inside the countproj FOR loop, we should add this code:

FOR colcheckobj AS INTEGER = 1 TO numofCPUobjects
           
      ' If the current projectiles is less that 4 pixels horizontally
      ' and vertically to an computer controlled object, diactivate
      ' that object and the projectile.
      IF (CPUObject(colcheckobj).Active = TRUE AND ABS(CPUObject(colcheckobj).X-Projectile(countproj).X) < 5 AND ABS(CPUObject(colcheckobj).Y-Projectile(countproj).Y) < 5) THEN
            ' Initiate some explosions (once you implement an explosion layer)
            ' Add score to player
            ' Etc.
            CPUObject(colcheckobj).Active = FALSE
            Projectile(countproj).ActiveTime = 0
      END IF
        
NEXT colcheckobj

This FOR loop goes through all the active (alive) computer controlled objects, checks for pixel distances between that object and the current projectile, and if the distance is less than 5 pixels horizontally and vertically, the computer controlled objects is deactivated (killed), as well as the projectile (since it exploded, right?).

This sub should be declared after the constants with:

DECLARE SUB DrawProjectiles ()

For the "object killing" to work, we need to place computer controlled objects drawing inside and IF clause that will draw them only if they are active. Like this (alter the countCPUobj FOR loop):

IF CPUobject(countCPUobj).Active = TRUE THEN
        LINE (CPUobject(countCPUobj).X, CPUobject(countCPUobj).Y)-(CPUobject(countCPUobj).X+sin(CPUobject(countCPUobj).AngleRad)*20,CPUobject(countCPUobj).Y-cos(CPUobject(countCPUobj).AngleRad)*20), RGB(2,117, 250)
        CIRCLE (CPUobject(countCPUobj).X, CPUobject(countCPUobj).Y), 3, RGB(2,117, 250)
END IF

The last thing that needs to be done is projectile initiation. A projectile will be initiated when the user presses SPACE. I recommend some other key for you, since SPACE doesn't work well with arrow keys (it's not always registered). I've been told this can't be fixed.

Put this code after the other main object's movement control code:

IF MULTIKEY(SC_SPACE) AND main_reload = 0 THEN
main_reload = 10
' The next line initiates a new projectile from the
' top of the main object's direction line, with the
' current main object angle, and using projectile
' type 1.
InitiateProjectile mainx+sin(anglerad)*20,mainy-cos(anglerad)*20, anglerad, 1
END IF
IF main_reload > 0 THEN main_reload = main_reload - 1

Observe how a new projectile is initiated from the top of the main object's direction line (the SIN and COS stuff), and with the main object's current angle (anglerad). What is the purpose of the main_reload variable? It's used to "time" the max ratio of auto-fire. When the user holds the fire key, a new projectile will only be fired every 10 loops since with every projectile initiation main_reload is set to 10, and a new projectile can be fired only when main_reload is 0. We need to add a line that reduces main_reload in every loop by 1, as I did it in the code above. Be sure to declare main_reload as a SHARED INTEGER variable, like it's done with other variables.

The entire code so far: codever4.txt

Compile it and enjoy.

Screenshot of the result:

And that's it for this tutorial. I hope you learned something useful and can expand on this tutorial with your own ideas and tricks.

The example programs with single and multiple computer controlled object's in source and compiled can be found here: dealing_with_angles_examples.zip

Mini-game from the "How To Program A Game With FreeBASIC - Lesson #2" tutorial compiled and with source for FreeBASIC ver.0.17: mini_game_FB017.zip


For the next tutorial I plan to add this:

I'm excited. Stay tuned!


A tutorial written by Lachie D. (lachie13@yahoo.com ; The Maker Of Stuff)