Angles in 2D environment and artificial smarts based on them - Part II
Written by Lachie Dazdarian (Semptember, 2007)
Introduction
Ok then. In the previous edition of this tutorial we stopped after creating an artificial smart algorithm which controlled multiple objects that followed around the screen an object controlled by the user. This was happening in a top-down 360° rotating environment, Star Control style.
What I promised last time to do is to show you how to implement basic scrolling (among other things) to this simple educational engine. So let's get busy!
Part One
Now how does one implement scrolling? Well, relatively easy, especially in our case as we don't feature tiled background or something like that only moving objects. Scrolling is simply implemented by deducting cameraX from all x coordinates used to draw our objects with drawing primitives, and cameraY from all y coordinates. cameraX being a variable that flags the X position of the camera and cameraY being a variable that flags the Y position of the camera (both setting the upper-left corner of the camera). Just observe this code snippet:
LINE (mainx-cameraX, mainy-cameraY)-(mainx-cameraX+sin(anglerad)*20,mainy-cameraY-cos(anglerad)*20), RGB(200, 0, 0) CIRCLE (mainx-cameraX, mainy-cameraY), 3, RGB(200, 0, 0)
Now what have we accomplished with that? Well nothing if cameraX and cameraY are 0 and/or don't change in the program. But if we "tell" the camera to lock onto the player's object, we'll get the desired result. The code that does this is as follows:
cameraX = mainx - screenmidx cameraY = mainy - screenmidy
Where screenmidx is the half width of the screen, while screenmidy is the half height of the screen, both of which we need to set (declare) before. Why do we need to use these variables? Allow me illustrate the entire problem with a nice...illustration.

As you see the camera is the size of the screen, meanining, it captures from (cameraX, cameraY) to (cameraX + screenwidth, cameraY + screenheight). Everything within this area will be visible. How come? Let's imagine that player's position is (2200, 3000). When will player's object be visible on the screen? When cameraX < 2200 and camerax + screenwidth > 2200, and when cameraY < 3000 and cameraY + screenheight > 3000. For example, if the screen resolution is 640*480 and camera position is (2000, 2700), it will "capture" from (2000, 2700) to (2640, 3180). The player's object will be drawn on (mainx - cameraX, mainy - cameraY) or (2200-2000, 3000-2700), which is (200,300), fully inside the visible screen. And that's the "trick" of scrolling in its essence.
In more graphics intensive games it is recommended you skip drawing of objects inside the map that are off the camera view, meaning in situations when object.x – camerax or object.y – cameray are less than 0 or above screenwidth/screenheight. You can do that with few simple IF clauses.
When telling the camera to lock onto an object and keep it in the center of the screen we need to use screenmidx and screenmidy because the CENTER of the camera is in (cameraX + screenmidx, cameraY + screenmid). In some situations you might have different requirements and might prefer camera not always centering the player's object. A somewhat different story is camera following (tracking) the player's or any other object (moving with slower speed than the tracked object), or moving using some path set with a script. Later on that.
I recommend putting these constants on the beginning of your program:
const screenwidth = 640 const screenheight = 480 const screenmidx = screenwidth/2 const screenmidy = screenheight/2
Where screenwidth and screenheight will correspond with the chosen screen resolution. After that you can simply set the screen resolution with:
SCREENRES screenwidth, screenheight, 32, 2, GFX_FULLSCREEN
cameraX and cameraY need to be declared as SHARED variables. Also, you need to declare two more variables, MapXMax and MapYMax, representing the size of the playfield (vertically and horizontally). In the following version of the engine I've set the player's starting position to (1000, 1000), playfield size to 2000*2000 (MapXMax = 2000; MapYMax = 2000), and randomized the computer controlled objects somewhere in the middle of the playfield (around the starting position of the player).
To keep the camera within the playfield we need to limit its movement. After...
cameraX = mainx - screenmidx cameraY = mainy - screenmidy
...we need to place the following code:
IF cameraX < 0 THEN cameraX = 0 IF cameraY < 0 THEN cameraY = 0 IF cameraX > MapXMax - screenwidth THEN cameraX = MapXMax - screenwidth IF cameraY > MapYMax - screenheight THEN cameraY = MapYMax - screenheight
The first two lines should be clear as cameraX and cameraY flag the upper-left corner of the very camera. The second two lines keep the camera not going off of the right and bottom edge of the map. If you observe picture 1 once more, everything should be clear. Since the bottom-right corner of the map is on (cameraX + screenwidth, cameraY + screenheight) this very coordinate must never cross the map edge (determined with MapXMax and MapYMax). screenwidth and screenwidth are placed on the right side of the two equations as we are reseting cameraX and cameraY, while MapXMax and MapYMax are always constant.
The only other piece of code I added is the one that draws the map edges so you can more easily visualize where the playfield ends. They are nothing but lines drawn around the map borders. This is the code:
LINE (MapXMax-1-camerax, MapYMax-1-cameray)-(0-camerax,MapYMax-1-cameray), RGB(255,255,255) LINE (MapXMax-1-camerax, 0-cameray)-(MapXMax-1-camerax,MapYMax-1-cameray), RGB(255,255,255) LINE (0-camerax, 0-cameray)-(MapXMax-1-camerax,0-cameray), RGB(255,255,255) LINE (0-camerax, MapYMax-1-cameray)-(0-camerax,0-cameray), RGB(255,255,255)
Which I placed above the DrawProjectiles call. Once more, everywhere where LINE and DRAW statements were used to draw objects that appear INSIDE the playfield x and y coordinates were deducted by cameraX and cameraY.
The altered source code: codever3.txt
What about different camera movement? I'll show you how to implement "dragging camera" effect, where the camera moves with slower speed that the object it is tracking. For this you need to declare the following variables with SINGLE precision, and change cameraX and cameraY to same.
DIM SHARED AS SINGLE cameraX, cameraY DIM SHARED AS SINGLE camera_xinertia, camera_yinertia DIM SHARED AS SINGLE camera_acceleration, camera_maxinertia DIM SHARED AS SINGLE Friction
How will the camera move in this case? With inertia! When, for example, the object that the camera is following is right from the camera, camera's X inertia will increase by camera's acceleration. In each loop the absolute value of camera's inertia will decrease (so it would stop when there is no movement) as the result of emulated friction, so the value of Friction needs to be less that camera_acceleration. Both x and y inertia mustn't become higher in absolute value than the maximum camera's inertia as the camera wouldn't move too fast and appear bouncy. This means that inertia (in both dimensions) will span from - max inertia to + max inertia.
I picked the following camera values, but you can play with them to get different results (put this with other variable initiations):
camera_maxinertia = 3 camera_acceleration = 0.4 Friction = 0.2
To move the camera in this manner replace...
cameraX = mainx - screenmidx cameraY = mainy - screenmidy
..with:
' Update camera. Increase camera's X or Y inertia according to the ' position of the player's object. IF cameraX < mainx - screenmidx THEN camera_xinertia = camera_xinertia + camera_acceleration IF cameraX > mainx - screenmidx THEN camera_xinertia = camera_xinertia - camera_acceleration IF cameraY < mainy - screenmidy THEN camera_yinertia = camera_yinertia + camera_acceleration IF cameraY > mainy - screenmidy THEN camera_yinertia = camera_yinertia - camera_acceleration ' Keep the x or y inertia under maximum inertia value. IF camera_xinertia > camera_maxinertia THEN camera_xinertia = camera_xinertia - camera_acceleration IF camera_xinertia < -camera_maxinertia THEN camera_xinertia = camera_xinertia + camera_acceleration IF camera_yinertia > camera_maxinertia THEN camera_yinertia = camera_yinertia - camera_acceleration IF camera_yinertia < -camera_maxinertia THEN camera_yinertia = camera_yinertia + camera_acceleration ' Reduce camera's inertia by Friction. IF camera_xinertia <> 0 and camera_xinertia > 0 THEN camera_xinertia = camera_xinertia - Friction IF camera_xinertia <> 0 and camera_xinertia < 0 THEN camera_xinertia = camera_xinertia + Friction IF camera_yinertia <> 0 and camera_yinertia > 0 THEN camera_yinertia = camera_yinertia - Friction IF camera_yinertia <> 0 and camera_yinertia < 0 THEN camera_yinertia = camera_yinertia + Friction ' Move the camera according to current inertia. cameraX = cameraX + camera_xinertia cameraY = cameraY + camera_yinertia
The code is pretty much self-explanatory. Note how the camera is actually moved (last two lines of code).
In the continuation of this text I'll use the original "lock onto target" camera movement.
So now after we implemented scrolling and set the playfield size we should add few more constraints to the player's and computer controlled objects, not allowing them to pass the map edges. I personally don't recommend such constraints as they are ugly and somewhat annoying to players. There is a better solution. Make the map circular. Meaning, when the player passes the bottom of the map he should appear on its top. This movement can be done continuously which is more difficult to code, or with a clear break (warping-like jumps). For this to work properly you need to instruct the artificial smarts to "see" the map edges and follow the player's ship ACROSS the map edges, which is again somewhat tricky to code. I won't go into this in this tutorial as this is something you should be able to code after reading this tutorial and would unnecessarily complicate this lesson. Still, if I receive more requests regarding circular playfield movement, I'll whip up another issue of this series.
To limit the movement of the player's object place the following code in the IF clause that moves the player if he pushes the up arrow key:
IF MULTIKEY(SC_UP) THEN mainx = mainx + sin(anglerad)*mainspeed mainy = mainy - cos(anglerad)*mainspeed IF mainx < 3 OR mainx > MapXMax - 3 THEN mainx = mainx - sin(anglerad)*mainspeed IF mainy < 3 OR mainy > MapYMax - 3 THEN mainy = mainy + cos(anglerad)*mainspeed END IF
Simple as that. This code prevents the player's object to pass the invisible borders 3 pixels away from the actual map borders. This depends on the size of your object/sprite and where its center is, so you will have to modify the previous code according to this. For example, if the size of the sprite representing the player is 20*20 and mainx and mainy flag the upper-right corner of it (and not its center like in our case), you would limit its movement to the right and bottom with mainx > MapXMax - 20 and mainy > MapYMax – 20, while its movement to the left and up with mainx < 0 and mainy < 0.
To limit the movement of the computer controlled objects we should do the same thing. The first two lines move the current computer controlled object and they are present from the first issue of this tutorial. The second two lines limit its movement.
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 IF CPUobject(countCPUobj).X < 3 OR CPUobject(countCPUobj).X > MapXMax - 3 THEN CPUobject(countCPUobj).X = CPUobject(countCPUobj).X - sin(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed IF CPUobject(countCPUobj).Y < 3 OR CPUobject(countCPUobj).Y > MapYMax - 3 THEN CPUobject(countCPUobj).Y = CPUobject(countCPUobj).Y + cos(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed
The remaining things I'm going to show you are just extra candy not directly related to scrolling. These include stars and planets in the playfield, smarter artificial smarts, and homing missiles.
Part Two
Let's start with adding stars and planets into our playfield.
First, let's declare the number of stars in the playfield with a constant. I picked 1000 stars.
const numofstars = 1000
Let's declare stars with ObjTyp user defined type (the one used with computer controlled objects), because it contains all the variables we need (x, y and Typ). Place the following code with the rest of variable declarations:
DIM SHARED Star(numofstars) AS ObjType
On the place where we initiate the position of computer controlled objects we should randomize the positions of our stars and their type (so that not all stars would look the same). Use the following code:
FOR initstar AS INTEGER = 1 TO numofstars
Star(initstar).X = INT(RND * 2000) + 1
Star(initstar).Y = INT(RND * 2000) + 1
Star(initstar).Typ = INT(RND * 6) + 1
NEXT initstar
We need now to construct a subroutine where the stars will be drawn. It's a rather simple one. Check the following code (put it at the end of the current code):
SUB DrawStars ()
' Loop through stars and draw them according to their type.
' Type 5 and type 6 are larger stars, and thus require more
' drawing primitives to be drawn.
FOR countstar AS INTEGER = 1 TO numofstars
SELECT CASE Star(countstar).Typ
CASE 1
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(255,255,255)
CASE 2
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(205,205,205)
CASE 3
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(100,100,100)
CASE 4
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(150,150,150)
CASE 5
LINE (Star(countstar).X-1-cameraX, Star(countstar).Y-cameraY)-(Star(countstar).X+1-cameraX, Star(countstar).Y-cameraY), RGB(90,90,90)
LINE (Star(countstar).X-cameraX, Star(countstar).Y-1-cameraY)-(Star(countstar).X-cameraX, Star(countstar).Y+1-cameraY), RGB(90,90,90)
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(250,250,250)
CASE 6
LINE (Star(countstar).X-1-cameraX, Star(countstar).Y-cameraY)-(Star(countstar).X+1-cameraX, Star(countstar).Y-cameraY), RGB(50,50,50)
LINE (Star(countstar).X-cameraX, Star(countstar).Y-1-cameraY)-(Star(countstar).X-cameraX, Star(countstar).Y+1-cameraY), RGB(50,50,50)
PSET (Star(countstar).X-cameraX, Star(countstar).Y-cameraY), RGB(170,170,170)
END SELECT
NEXT countstar
END SUB
Inside the subroutine we have a FOR loop that loops through all the stars and draws them on their appropriate positions according to their type. Note the cameraX and cameraY variables. Everything draw within the playfield must be drawn using cameraX and cameraY. Declare this subroutine with the remaining subroutine declarations and call the subroutine after CLS in the main loop simply with:
DrawStars
Almost on the same way the planets should be drawn, we'll just have less of them in the playfield. I've chosen only to have 4 planets it the playfield, and 3 types of them.
Declare this constant:
const numofplanets = 4
Declare our planets variable:
DIM SHARED Planet(numofplanets) AS ObjType
Initiate our planets with:
FOR initplanet AS INTEGER = 1 TO numofplanets
Planet(initplanet).X = INT(RND * 1800) + 100
Planet(initplanet).Y = INT(RND * 1800) + 100
Planet(initplanet).Typ = INT(RND * 3) + 1
NEXT initplanet
Construct a subroutine that draws our planets as shown:
SUB DrawPlanets ()
' Loop through planets and draw them according to their type.
FOR countplanet AS INTEGER = 1 TO numofplanets
SELECT CASE Planet(countplanet).Typ
CASE 1
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 20, RGB(168,24,24),,,,F
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 20, RGB(148,4,4)
CASE 2
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 40, RGB(255,255,128),,,,F
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 40, RGB(205,205,88)
CASE 3
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 25, RGB(0,192,0),,,,F
CIRCLE (Planet(countplanet).X-cameraX, Planet(countplanet).Y-cameraY), 25, RGB(0,152,0)
END SELECT
NEXT countplanet
END SUB
Declare the subroutine and call it after DrawStars call (since we want to draw planets OVER stars).
The entire code so far: codever4.txt
Compile it and you should get something like this:

Neat, eh?
So what's next? Let's implement somewhat more complex artificial smarts. Here we go.
I'll implement only two main types of artificial smarts as we want to keep this simple. One will travel between two points and only follow the player if the player is very near it. The second will attack the player and exchange between attack and run modes. I should just say that these artificial smarts are only for educational purposes. I'm teaching you the concepts of creating artificial smarts, not how to create GOOD artificial smarts. This is up to you and is what will eventually make your game better that a similar one.
For the purpose of the extended artificial smarts code we need to input two more variables in the ObjTyp user defined type. These are ASMode that will flag the main AS mode of a ship, and ASSubMode that will flag the sub mode of a specific AS (like if the ship is in run or attack mode). The new ObjTyp user defined type should look like this:
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.).
ASMode AS INTEGER ' Used to flag the artificial smart type of a ship.
ASSubMode AS INTEGER ' Used to flag the artificial smart sub mode of a ship (attacking, running, etc.)
END TYPE
Also, it is recommended we declare 3 extra variables (as SHARED and INTEGER) named targetX, targetY and ASdirection.
targetx and targety will flag the target that the computer controlled object should follow or run away from, and using these two variables we can change the behavior of computer controlled objects quite easily. In one word, this gives us flexibility. The third variable, ASdirection, will be used to change the rotation of the computer controlled object when tracking a target, meaning, will it rotate toward or away from that target.
Now let's code the first artificial smart (ASMode = 1), the one traveling from one point to another. It will have two sub modes. When ASSubMode = 1 then it will be going to toward point A, while when ASSubMode = 2 then it will be going toward point B. But the AS will travel to any of these points only if the player's ship is not near enough (80 pixels away in both directions).
We should first alter the beginning of our AS code by implementing the targetX and targetY variables:
' If ASMode is true apply the artificial smart code on the current CPU controlled object
IF ASMode = TRUE THEN
' By default the target is player's object.
targetX = mainx
targetY = mainy
' By default computer controlled object rotates TOWARD
' the target.
ASdirection = 1
After this code put the the following code:
' If the computer controlled object's ASMode = 1 and
' the player's object is not near him travel to point A (120, 120)
' or point B (1700,400) according to ASSubMode.
IF CPUobject(countCPUobj).ASMode = 1 THEN
IF ABS(CPUobject(countCPUobj).X-mainx)>80 OR ABS(CPUobject(countCPUobj).Y-mainy)>80 THEN
IF CPUobject(countCPUobj).ASSubMode = 1 THEN
targetX = 120
targetY = 120
END IF
IF CPUobject(countCPUobj).ASSubMode = 2 THEN
targetX = 1700
targetY = 400
END IF
' Change AS sub mode if reached one or the other point.
IF ABS(CPUobject(countCPUobj).X-targetX)<20 AND ABS(CPUobject(countCPUobj).Y-targetY)<20 THEN
IF CPUobject(countCPUobj).ASSubMode = 1 THEN
CPUobject(countCPUobj).ASSubMode = 2
ELSE
CPUobject(countCPUobj).ASSubMode = 1
END IF
END IF
END IF
END IF
The previous code should be self-explanatory. Just have in mind you can play with this AS by adding more AS sub modes (travel points) and by changing the coordinates of these points. And note how this code is skipped if the player's object is less than 80 pixels away in both directions. In that case the computer controlled object simply skips to the usual AS that tracks the player (default target).
Do not forget to alter the resultanglerad equation since now it will be calculated according to targetX and targetY.
resultanglerad = ATAN2((-1)*(targetY-CPUobject(countCPUobj).Y),(targetX-CPUobject(countCPUobj).X))
Let's now do the second artificial smart. This one will employ an attack and run tactic. It will engage the player until very near it after which it will switch to run mode. In run mode it will run away from the player until far enough when it will switch back to attack mode. Switching between attack and run will be done using the ASdirection variable. We need to alter the equations that change the angle of the computer controlled object by multiplying RotationSpeed with ASdirection. For one of the four equations that need to be changed this would look like this:
IF (360-CPUobject(countCPUobj).AngleDeg+resultangledeg) >= (CPUobject(countCPUobj).AngleDeg-resultangledeg) THEN CPUobject(countCPUobj).AngleDeg = CPUobject(countCPUobj).AngleDeg - CPUobject(countCPUobj).RotationSpeed*ASdirection
So when ASdirection is 1 (the default behavior) the computer controlled object rotates toward the player. But when it's -1 the computer controlled object rotates away from the player, and if thrusting it moves away from the player. Put the following code after the first AS code:
' If the computer controlled object's ASMode = 2
' swap between attack (ASdirection = 1) and
' run modes (ASdirection = - 1).
IF CPUobject(countCPUobj).ASMode = 2 THEN
IF CPUobject(countCPUobj).ASSubMode = 1 THEN
' Attack mode. Swap to run mode if
' less than 50 pixels away in both directions
' from the target.
ASDirection = 1
IF ABS(CPUobject(countCPUobj).X-targetX)<50 AND ABS(CPUobject(countCPUobj).Y-targetY)<50 THEN CPUobject(countCPUobj).ASSubMode = 2
END IF
IF CPUobject(countCPUobj).ASSubMode = 2 THEN
' Run mode. Swap to attack mode if
' more than 200 pixels away in any direction
' from the target.
ASDirection = -1
IF ABS(CPUobject(countCPUobj).X-targetX)>200 OR ABS(CPUobject(countCPUobj).Y-targetY)<200 THEN CPUobject(countCPUobj).ASSubMode = 1
END IF
END IF
This code should also be self-explanatory. Again, you can play with this AS by changing the distances on which the computer controlled objects swaps from one mode to another. You can also add more juice to it by timing the run mode. For example, if the player decides to chase the running computer controlled object, it would be nice that this object stops running after a while and starts to attack again. But this is really a large thing to ponder on. Like I said, use this tutorial only to grasp to basic concept of AS creation.
I also recommend you to add a thrust or engage variable and connect it with the computer controlled objects' movement equations. For example, if you would want for the computer controlled object not to move, thrust would be 0. Otherwise it would be 1. Connected with the movement equations it would look like this:
CPUobject(countCPUobj).X = CPUobject(countCPUobj).X + sin(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed*thrust
CPUobject(countCPUobj).Y = CPUobject(countCPUobj).Y - cos(CPUobject(countCPUobj).AngleRad)*CPUobject(countCPUobj).Speed*thrust
By using this variable with artificial smarts you can instruct computer controlled objects to stop in certain situations. Like if you would want them to approach the player, stop and start shooting.
For the AS code to work we need to initiate ASModes of our computer controlled objects and their default ASSubModes. I've decided that half of the computer controlled objects will use AS mode 1, while the other half will employ AS mode 2.
' We loop through our cpu controlled object and initiate
' their variables.
FOR initCPUobj AS INTEGER = 1 TO numofCPUobjects
CPUobject(initCPUobj).X = INT(RND * 600) + 720 ' Randomize cpu object's position from 20 to 620
CPUobject(initCPUobj).Y = INT(RND * 440) + 820 ' 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.
CPUobject(initCPUobj).ASMode = 2
IF initCPUobj < 11 THEN CPUobject(initCPUobj).ASMode = 1
CPUobject(initCPUobj).ASSubMode = 1
NEXT initCPUobj
The entire code so far: codever5.txt
Phew. The AS is done. What remains is adding ability to shoot projectiles to computer controlled objects, implementing homing projectiles for the player, and replace those ugly circles with lines into rotating triangles.
Adding projectile shooting ability to computer controlled objects is relavively simple. First, we need to call InitiateProjectile sub inside the computer controlled objects' FOR loop on the appropriate location and if specific conditions are met. Just place the following code above the code that draws our computer controlled objects, inside the IF CPUobject(countCPUobj).Active = TRUE clause:
IF CPUobject(countCPUobj).Reload = 0 AND ABS(CPUobject(countCPUobj).X-targetX)<120 AND ABS(CPUobject(countCPUobj).Y-targetY)<120 THEN
CPUobject(countCPUobj).Reload = 35
InitiateProjectile CPUobject(countCPUobj).X+sin(CPUobject(countCPUobj).AngleRad)*20,CPUobject(countCPUobj).Y-cos(CPUobject(countCPUobj).AngleRad)*20, CPUobject(countCPUobj).AngleRad, 5
END IF
IF CPUobject(countCPUobj).Reload > 0 THEN CPUobject(countCPUobj).Reload = CPUobject(countCPUobj).Reload - 1
It is very similar to the one that initiates a projectile when the player pushes space. In this code the computer controlled object will shoot a projectile if the player's object is less than 120 pixels away horizontally and vertically, and if the computer controlled object's weapon is reloaded. As you see I'm using the same reload method like with the player. Note I initiate projectile type 5 with computer controlled objects because I decided for projectile types above 4 to represent projectiles that "hurt" the player, while projectile types less than 5 "hurt" computer controlled objects. Don't forget to add the Reload variable in the ObjTyp custom defined type.
For this code to work we need to alter the InitiateProjectile and DrawProjectiles subroutines to "support" the new projectile type.
In the InitiateProjectile sub we should add this IF clause after the same one for projectile type 1:
IF Projectile(initproj).Typ = 5 THEN Projectile(initproj).Speed = 3
We can do this differently if we, for example, want for projectile type 5 (while being slower that the player's object) to last shorter amount of time or perhaps longer. Feel free to use the Typ variable to set the desired characteristics of your projectiles, whatever they might be.
We should also alter the DrawProjectiles sub as it follows:
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.
SELECT CASE Projectile(countproj).Typ
CASE 1
LINE (Projectile(countproj).X-cameraX, Projectile(countproj).Y-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(192, 192, 0)
CASE 5
LINE (Projectile(countproj).X-cameraX, Projectile(countproj).Y-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(222, 10, 0)
END SELECT
' The next FOR loop checks for collision with all the
' active computer controlled objects, and if collision is
' preset (pixel distance check), we deactivate (kill) that
' computer controlled object.
FOR colcheckobj AS INTEGER = 1 TO numofCPUobjects
' If the current projectiles is less that 4 pixels horizontally
' and vertically to an computer controlled object, if the projectile
' type is less than 5 (a projectile shot by the player) diactivate
' that object and the projectile.
IF (Projectile(countproj).Typ < 5 AND 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
' If the current projectiles is less that 4 pixels horizontally
' and vertically from the player, if the projectile
' type is more than 4 (not player's projectile) reduce
' player's energy and diactivate the projectile.
IF (Projectile(countproj).Typ > 4 AND ABS(mainx-Projectile(countproj).X) < 5 AND ABS(mainy-Projectile(countproj).Y) < 5) THEN
' Initiate some explosions (once you implement an explosion layer)
' Reduce player's energy
' "Kill" him if energy < 0
' Etc.
Projectile(countproj).ActiveTime = 0
END IF
NEXT colcheckobj
END IF
NEXT countproj
END SUB
I changed the drawing of projectiles (instead of IF clauses I used SELECT CASE and added drawing for projectile 5 (I set its color to red)) and separated projectile collision with the computer controlled objects from that with the player. Projectiles with Typ < 5 will harm the computer controlled objects, while projectiles with Typ > 4 will harm the player. For the purpose of this example engine the player can't be destroyed (its energy is not reduced).
The last thing related to weapons I'll do is implement a secondary weapon to the player - homing missiles. Homing missiles are rather simple to add into this code. All we need to do is add one more projectile type and implement the basic AS used with the computer controlled objects with it.
First, let's add code that will initiate projectile type 2 when the user pushes M. Place the following code after the IF clause that initiates projectile type 1:
IF MULTIKEY(SC_M) AND main_reload = 0 THEN
main_reload = 20
InitiateProjectile mainx+sin(anglerad)*20,mainy-cos(anglerad)*20, anglerad, 2
END IF
Nothing to say here but that I made for the homing missiles to reload slower. We could use here multiple reload variables (one for each weapon), but not to complicate let's leave it as it is.
We should now add another IF clause in the InitiateProjectile sub as follows:
IF Projectile(initproj).Typ = 2 THEN
Projectile(initproj).Speed = 4
Projectile(initproj).RotationSpeed = 4
IF MULTIKEY(SC_UP) THEN Projectile(initproj).Speed = Projectile(initproj).Speed + mainspeed
END IF
Note that I need to initiate a rotation speed for this projectile as it will have to rotate (change its angle) in order to follow the object it is currently tracking.
Inside the DrawProjectiles sub we need to add the code that will draw this new projectile:
SELECT CASE Projectile(countproj).Typ
CASE 1
LINE (Projectile(countproj).X-cameraX, Projectile(countproj).Y-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(192, 192, 0)
CASE 2
LINE (Projectile(countproj).X-cameraX, Projectile(countproj).Y-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(200, 200, 200)
LINE (Projectile(countproj).X+sin(Projectile(countproj).Anglerad)*2-cameraX, Projectile(countproj).Y-cos(Projectile(countproj).AngleRad)*2-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(222, 0, 0)
CASE 5
LINE (Projectile(countproj).X-cameraX, Projectile(countproj).Y-cameraY)-(Projectile(countproj).X-cameraX+sin(Projectile(countproj).Anglerad)*3,Projectile(countproj).Y-cameraY-cos(Projectile(countproj).AngleRad)*3), RGB(222, 10, 0)
END SELECT
Inside the FOR colcheckobj loop we need to place the following code which is basically the artificial smart for our homing missile:
' If the current projectile is typ 2 (homing missile)
' instruct it to follow the closest computer controlled
' object. Same artificial smart code as with computer
' controlled objects.
IF (Projectile(countproj).Typ = 2 AND CPUObject(colcheckobj).Active = TRUE AND ABS(CPUObject(colcheckobj).X-Projectile(countproj).X) < 100 AND ABS(CPUObject(colcheckobj).Y-Projectile(countproj).Y) < 100) THEN
' Missile always moving TOWARD the target and
' target always the closest computer controlled
' object.
ASdirection = 1
targetY = CPUObject(colcheckobj).Y
targetX = CPUObject(colcheckobj).X
resultanglerad = ATAN2((-1)*(targetY-Projectile(countproj).Y),(targetX-Projectile(countproj).X))
resultanglerad = PI/2 - resultanglerad
IF resultanglerad < 0 THEN resultanglerad = resultanglerad + 2*PI
resultangledeg = (resultanglerad*180)/PI
Projectile(countproj).AngleDeg = (Projectile(countproj).AngleRad*180)/PI
IF Projectile(countproj).AngleDeg > resultangledeg THEN
IF (360-Projectile(countproj).AngleDeg+resultangledeg) >= (Projectile(countproj).AngleDeg-resultangledeg) THEN Projectile(countproj).AngleDeg = Projectile(countproj).AngleDeg - Projectile(countproj).RotationSpeed*ASdirection
IF (360-Projectile(countproj).AngleDeg+resultangledeg) < (Projectile(countproj).AngleDeg-resultangledeg) THEN Projectile(countproj).AngleDeg = Projectile(countproj).AngleDeg + Projectile(countproj).RotationSpeed*ASdirection
END IF
IF Projectile(countproj).AngleDeg < resultangledeg THEN
IF (360-resultangledeg+Projectile(countproj).AngleDeg) >= (resultangledeg-Projectile(countproj).AngleDeg) THEN Projectile(countproj).AngleDeg = Projectile(countproj).AngleDeg + Projectile(countproj).RotationSpeed*ASdirection
IF (360-resultangledeg+Projectile(countproj).AngleDeg) < (resultangledeg-Projectile(countproj).AngleDeg) THEN Projectile(countproj).AngleDeg = Projectile(countproj).AngleDeg - Projectile(countproj).RotationSpeed*ASdirection
END IF
IF Projectile(countproj).AngleDeg<0 THEN Projectile(countproj).AngleDeg=Projectile(countproj).AngleDeg+360
IF Projectile(countproj).AngleDeg>359 THEN Projectile(countproj).AngleDeg=Projectile(countproj).AngleDeg-360
Projectile(countproj).AngleRad = (Projectile(countproj).AngleDeg*PI)/180
EXIT FOR
END IF
Note the EXIT FOR which is used because I need to exit the FOR loop once a target is found. Guess what would happen if the homing projectile was flying in between two computer controlled objects and there was no EXIT FOR. The projectile would try to follow both of the objects and on the end wouldn't follow either one (rotation in both directions would nullify each other).
The entire code so far: codever6.txt
Play with it, and don't forget to push M to fire homing missiles.
I'm not sure how many of you are still with me, but we have one more final thing to do. We'll replace those ugly circles with lines into pretty rotating triangles. How? Simple, with some basic trigonometry introduced in the first lesson.
I will explain two methods of drawing rotated objects (consisted of lines connected with points) in 2D space.
Both include setting the center of your object which doesn't have to be the geometrical center, although with the first method (polar) it is highly recommended you choose the geometrical center. The object center is just a point from which you calculate all the other points of the object relative to the center point. In our case we have a triangle. On the following picture I've set the center of the object in the geometrical center. Points 1, 2 and 3 define the triangle.

In the polar method we'll use polar coordinate system logic to rotate these 3 points. We'll define our 3 points with a radius and an angle that these points close with the center and vertical line going from the center toward up (zero angle axis in our coordinate system - lesson 1). In our case point 1 is determined with the angle of 0 (or 2*PI) and radius1. Point 2 is determined with the angle of PI*3/4 (135°) and radius2, while point 3 is determined with the angle of PI*5/4 (225°) and radius3. Also, in this specific case radius1 = radius2 = radius3, but often this is not the case as you will probably want to rotate other things than equilateral triangles.
The following equations should be self-explanatory and result from the above method. All angles are in radias, and object_angle is basically the angle in radians by which the object is rotated from the 0 angle position.
Position of point 1 before rotation (blue triangle): centerx + sin(0) * radius1, centery - cos(0) * radius1 Position of point 2 before rotation: centerx + sin(0+PI*3/4) * radius2, centery - cos(0+PI*3/4) * radius2 Position of point 3 before rotation: centerx + sin(0*5/4) * radius2, centery - cos(0+PI*5/4)* radius2 Position of point 1 after rotation (red triangle): centerx + sin(object_angle) * radius1, centery - cos(object_angle) * radius1 Position of point 2 after rotation: centerx + sin(object_angle+PI*3/4) * radius2, centery - cos(object_angle+PI*3/4) * radius2 Position of point 3 after rotation: centerx + sin(object_angle*5/4) * radius2, centery - cos(object_angle+PI*5/4) * radius2
In our code for computer controlled objects it looks like this:
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*10)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad+PI*3/4)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad+PI*3/4)*10), RGB(2,117, 250)
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad+PI*3/4)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad+PI*3/4)*10)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad+PI*5/4)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad+PI*5/4)*10), RGB(2,117, 250)
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad+PI*5/4)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad+PI*5/4)*10)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*10), RGB(2,117, 250)
The LINE statements connect the 3 triangle defining points. 10 is the chosen radius. Change it for smaller/bigger triangles.
The Cartesian method uses Cartesian coordinate system logic and I don't consider it suitable as the polar method for this problem, but I will show it for educational purposes. Observe the following picture which illustrates how to rotate a single point. We don't need to complicate this with more points as a line is nothing but two points connected.

If you observe the picture, after rotation we need to determine x2 and y2 by knowing the origin point (x1, y1) and the angle by which the object has rotated (object_angle). We need to break this problem in two parts.
We know how to rotate a point which is on y axis (for x = 0) when the rotation angle is 0. We did that in lesson 1 with the line showing the direction of ships. It's done by adding +sin(object_angle)*y_distance to x coordinate, and -cos(object_angle)*y_distance to y coordinate, where y_distance is the distance of the point from the x axis. When the point is not lying on the y axis (x <> 0) then during rotation we also need to add +cos(object_angle)*x_distance to x coordinate, and +sin(object_angle)*x_distance to y coordinate, where x_distance is the distance of the point from the y axis when the rotation angle is 0. So we would need to have this sort of formulae for coordinates:
(centerx + sin(object_angle)*y_distance + cos(object_angle)*y_distance, centery - cos(object_angle)*y_distance + sin(object_angle)*x_distance)
Yeah, scary. Have in mind x_distance is positive when the point is right from the center of the object, and negative when the point is left from the center of the object. y_distance is positive when the point is above the center, and negative when the point is below the center. In our case for the computer controlled objects this method would result in something like this (approximately):
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*10+cos(CPUobject(countCPUobj).AngleRad)*0, CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*10+sin(CPUobject(countCPUobj).AngleRad)*0)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*-10+cos(CPUobject(countCPUobj).AngleRad)*-10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*-10+sin(CPUobject(countCPUobj).AngleRad)*-10), RGB(2,117, 250)
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*-10+cos(CPUobject(countCPUobj).AngleRad)*-10, CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*-10+sin(CPUobject(countCPUobj).AngleRad)*-10)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*-10+cos(CPUobject(countCPUobj).AngleRad)*10,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*-10+sin(CPUobject(countCPUobj).AngleRad)*10), RGB(2,117, 250)
LINE (CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*-10+cos(CPUobject(countCPUobj).AngleRad)*10, CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*-10+sin(CPUobject(countCPUobj).AngleRad)*10)-(CPUobject(countCPUobj).X-cameraX+sin(CPUobject(countCPUobj).AngleRad)*10+cos(CPUobject(countCPUobj).AngleRad)*0,CPUobject(countCPUobj).Y-cameraY-cos(CPUobject(countCPUobj).AngleRad)*10+sin(CPUobject(countCPUobj).AngleRad)*0), RGB(2,117, 250)
Yikes! So I'm sure the choice is quite clear. Go with the polar method.
The final code: codever7.txt
If you compile the last code you should get something like this:

You'll notice the player's ship is somewhat different. Check the code to see how I draw it. Also, collision hit zones were changed (as the ships are now bigger) and the locations from which the projectiles are initiated.
Download the final example engine compiled: multi_object_rotation_engine.zip
Download the final example with angles calculated using the geometrical standard (east it 0 degrees, positive rotation is counter-clockwise; due popular demand): multi_object_rotation_engine_geomstand.zip
If you prefer rotating sprites over rotating triangles and such, you might want to check D.J.Peters' MultiPut routine (multiput_nov06.bas), or counting_pine's unofficial release of the same routine (multiput_unofficial.bas).
And that would be it. I hope you learned something new. You are encouraged to expand on topics covered in this tutorial or come up with better solutions for some of the problems discussed here. I am sure many things in this example engine could have been done better, but all of this was to show you HOW to do certain things, and do not represent the best way to do them.
Until some other tutorial. I have few I want to write, but now I want to concentrate in rewriting and revising my older tutorials so they would compile in FB ver.0.18b.
Cheers!