Lesson 17: Movement and Rotation
In a few of the previous lessons we've encountered code that moves objects around on the screen. It's time to explore the various methods that motion can be achieved using vectors, degrees, radians, and rotation.
At some point you're going to need trigonometry when making games. I know, I know, that word sounds scary and it's often at this point where budding game programmers give up. Searching the Internet for information on how to add motion and physics to games can be mind numbing. Many times the solutions provided by individuals is cryptic and down-right confusing because they assume you already have a grasp of higher math functions. That was my issue when I started writing games. The answers were there but presented in such a way that it confused me. You're not alone. The highest math I took in high school was Algebra II and by the time I was writing games those lessons were a faded memory. In this lesson I hope to break these concepts down to something everyone can understand. The math involved is surprisingly easy once you get your head wrapped around it.
Don't let the math turn you away. If you are having trouble understanding the concepts presented in this lesson head on over to the QB64 Forum and ask for help. There are no stupid questions!
All objects on a computer screen use coordinates to identify where they are located. The CIRCLE statement requires a single set of coordinates to identify its center location on the screen. The LINE statement requires two sets of coordinates to identify the start and end locations of the line segment on the screen. Yet another command we will investigate shortly is _MAPTRIANGLE that requires three coordinate pairs. By modifying an object's coordinates you can effectively move an object around the screen. The direction in which an object moves is known as a vector.
A vector is a directed quantity that contains a length and direction but not position. For example lets place a circle at coordinate 200,100 on the screen. In this case the circle's X coordinate is 200 and its Y coordinate is 100. If you want to move the circle up and to the right five pixel coordinates you would need to add 5 to the X coordinate and subtract 5 from the Y coordinate. This moves the circle to the right (X = X + 5) and up (Y = Y - 5). If placed into a loop the circle will continue to move in this direction or vector:
x = 200 ' circle's x coordinate
y = 100 ' circle's y coordinate
CIRCLE(x, y), 30 ' draw circle
x = x + 5 ' move circle to the right 5 pixels
y = y - 5 ' move circle up 5 pixels
This code snippet set up two vectors for the circle. The circle has an x vector of 5 and a y vector of -5. It doesn't matter where the circle's coordinates start since all that matters to a vector is how long it is (the quantity) and the direction it travels in (the quantity's sign). The circle could be placed at 300,300 but these vector quantities will always result in the circle moving up and to the right. The vector quantities can be placed in their own variables like so:
x = 300 ' circle's x coordinate
y = 300 ' circle's y coordinate
Vx = 5 ' x vector of 5 (right)
Vy = -5 ' y vector of -5 (up)
CIRCLE(x, y), 30 ' draw circle
x = x + Vx ' move circle x vector direction
y = y + Vy ' move circle y vector direction
By varying the values of Vx and Vy you can effectively have the circle move in any direction you wish. Here is a simple example showing this in practice. The file BallVectorDemo.BAS can be found in your .\tutorial\Lesson17\ directory.
Lines 15 and 16 of the code set up random vector quantities of -5 to 5 for both the X and Y vectors. Lines 20 and 21 update the circle's position by adding these vector quantities to the circle's current position. If the circle hits the side of the screen as determined by lines 22 and 25 the sign of the vector is changed. This effectively reverses the circle's direction by altering the vector by 180 degrees.
In the example code above the speed of the circle is a fixed quantity. In order to change the speed of the circle the length of the vectors (the quantity) needs to be changed. This is typically done by multiplying the vector quantity by a given speed. However the code above produces vector quantities in the range of -5 to 5 and multiplying by a fixed speed value will produce an inconsistent result. The best way to handle this is to have vectors stay within the range of -1 to 1 producing a speed that is consistent. This is done by normalizing the the vectors into unit vectors with values between -1 and 1.
To normalize a vector you simply set up a right triangle using the vector quantities. Then use the Pythagorean Theorem to calculate the length of the vector (the hypotenuse) and finally divide the original vector quantities by the calculated length. Figure 1 below demonstrates this:
Figure 1: Normalizing vector quantities
The Vx and Vy vector quantities are calculated by subtracting the x values and y values to obtain the vector lengths. You may be wondering why bother since we already know the vector quantities. We'll get to that a little later in the lesson. Vx and Vy are now side A and side B of the right triangle. The length of the vector is calculated using the Pythagorean Theorem. Finally, that length is used to divide the original Vx and Vy vector quantities producing a result of -1 to 1. Now a speed multiplier can properly be applied to the vector quantities as the following example program illustrates. This program BallSpeedDemo.BAS can be found in your .\tutorial\Lesson17\ directory.
In lines 18 through 22 we used the formula in Figure 1 above to normalize the vectors to between -1 and 1. In lines 34 and 35 the vector quantities are added to the circle's location just like before but this time the value in speed% is multiplied to the vector quantity effectively increasing the number of pixels the circle moves in the same direction. The value in speed% is kept between 1 and 20 guaranteeing 1 to 20 pixel movements for the circle. In the first code example this could not be guaranteed because the vectors were not normalized. Each time the code would have been run the maximum speed would have varied wildly.
The _HYPOT Statement
The SQR statement (square root) was used in the previous two code examples to calculate the length of the vector using the Pythagorean Theorem. SQR is used to calculate the hypotenuse of the triangle that the theorem works with. However, SQR is a very slow statement which is why QB64 offers the _HYPOT statement. The _HYPOT statement calculates the hypotenuse given the other two sides of the triangle:
Length! = _HYPOT(SideA!, SideB!) ' calculates the hypotenuse
You simply supply the side lengths and don't need to worry about squaring them first. _HYPOT is faster than using SQR so for the rest of this course _HYPOT will be used in lieu of SQR for length and distance calculations. The previous two code examples only control one object on the screen and you would not be able to tell the difference between SQR and _HYPOT. However, when you start creating games that control hundreds of objects on the screen these optimizations make a huge difference.
Up to this point all of the example code has had an object rambling around with no purpose other than to bounce off the sides of the screen. In games you want enemies to chase you and their bullets to fire in your direction. You also want to be able to shoot your bullets in any given direction you desire. Using vectors is one of the simplest methods of achieving this. Remember these calculations in Figure 1 above?
Vx = x2 - x1
Vy = y2 - y1
These two statements now come into full use when you want to make a vector that points toward another object. Instead of using the coordinate (0, 0) for that start of the vectors as Figure 1 demonstrates you would use the coordinates of the object that you want to get a vector from. The end of the vectors are the coordinate of the object you are pointing to.
Remember, a vector does not care about location as it only contains length and direction. So a vector coordinate starting at one object and ending at another object's coordinates is perfectly fine, it's still a vector, a simple directed quantity. You simply make a right triangle between the two objects and solve for the equation in Figure 1 above. The result will be a normalized vector pair that can be applied to another object, such as a bullet, that flies from the first object to the second one. Or you can use the vector pair to direct the from object towards the to object as the following example code illustrates. This code can be found as TagVectorDemo.BAS in your .\tutorial\Lesson17\ directory.
Figure 2: Tag, You're it!
By calculating the vector between the two circles and applying that vector to the red circle it becomes "smart" and can directly follow the green circle that you control. Before we get into the code a few things to note:
Line 36 ends in an underscore ( _ ) character which means the remainder of the statement is contained in line 37. Remember, an underscore can be used to segment large lines of code into multiple lines for easier viewing.
The formula in Figure 1 has been turned into a function called P2PVector that takes two x,y coordinate pairs as input and returns a normalized vector pair.
P2PVector FromPoint, ToPoint, NormalizedVector
Since vectors come in pairs a TYPE definition named XYPAIR has been created to easily handle them. This same TYPE definition can also hold x,y coordinate pairs so we get bonus double duty from it.
Lines 8 through 12 create a TYPE definition for a circle object containing an x,y coordinate pair, an x,y vector pair, and a radius. The x,y coordinate pair is a circle's current location and the x,y vector pair is the direction a circle is traveling in.
Lines 14 and 15 are setting up the player and enemy circle objects that can be manipulated around the screen.
The player's position is updated and drawn in lines 28 through 32.
Line 33 is where the magic happens:
P2PVector RCir.loc, GCir.loc, RCir.vec
The red circle and green circle's positions are passed to the function where the vector normalizing formula is performed. The normalized vectors are then returned and placed into RCir.vec. P2PVector is a subroutine and unlike a function it can't return a result in its name so RCir.vec that is passed by reference is modified within the subroutine and those changes are returned. The red circle's vector values have now been altered and point directly at the green circle.
In line 38 a function called P2PDistance is passed both circle's locations and if the distance between the circles is greater than both radii added together the red circle's location is updated in lines 39 and 40. Line 38 is a basic circle collision detector. Lines 39 and 40 use the updated vector values to move the red circle in the direction of the green circle.
Loop back around and do the whole process again. No matter how often the green circle is moved around line 33 will guarantee that the normalized vectors are calculated to keep the red circle traveling toward the green circle.
The white line inside the red circle that always points towards the green circle is created in line 36 (and 37). A line is drawn from the center of the red circle to a point in the same direction as the red circle's vector value multiplied by its radius. It's the same concept as multiplying the vector by a speed, just in this case we use the circle's radius as one big leap.
Controlling Multiple Objects
Using vectors along with arrays allows you to control many directed objects on the screen at once as the following example code illustrates. Move the cross hair around the screen with the mouse and use the left mouse button to fire a bullet. Hold the left mouse button to increase the fire power and speed of the bullet. This code can be found as VectorShootDemo.BAS in your .\tutorial\Lesson17\ directory.
Figure 3: The start of a bullet hell game
All of the action controlled on the screen is done with nothing more than normalized vectors. All of the variables needed to manipulate each individual bullet is held in a dynamic array named Bullet(). The dynamic array is allowed to grow as needed to accommodate more bullets and reset when no active bullets are present. Each bullet's location ( Bullet().loc ), vector ( Bullet().vec ), speed ( Bullet().speed ), color ( Bullet().col ), and status ( Bullet().active ) are all maintained in the array using the TYPE definition BULLET.
The DrawBullets subroutine is used to draw active bullets onto the screen with the information provided in the array. If the subroutine detects there are no active bullets left the Bullet() array it is reset back to a zero index. This ensures that the array only grows as large as needed for any given amount of bullets on the screen.
The FreeIndex function scans the array for any indexes where the bullets have become inactive and returns that index value to place the next bullet into. If the function detects that there are no free index values available it increases the array size by one and passes that new index value back to place the next bullet into. By reusing inactive indexes and only increasing the array size when necessary the array size is kept as small as possible at all times. If you are familiar with "Bullet Hell" games you know there can literally be hundreds of bullets flying around the screen at any given time. Using a fixed static array would be possible to track them all but the method of using an array that can adjust to the amount of objects needed is ideal.
Where are the variable type identifiers?
If you haven't noticed yet you don't need to use variable type identifiers, such as % for integer and ! for single, to identify a variable's type. The first two code examples use type identifiers on all of the variables. However the following two code examples did not. These two lines mean exactly the same thing:
DIM Count AS INTEGER
As well as these:
DIM KeyPress AS STRING
You can even control the size of strings by doing this:
DIM KeyPress AS STRING * 1 ' a string variable with a length of one
We discussed in an earlier lesson that the variable declarations inside of TYPE definitions require that variables type identifiers be spelled out:
x AS SINGLE
Y AS SINGLE
However this method of identifying variable types can be used everywhere. This is a personal preference of the programmer. The main advantage of using type identifiers is that it makes it easy to know the type a variable is no matter where it resides in the source code. When code starts getting very large all of the type identifiers can make code seem cluttered (in my opinion, perhaps not another programmer's). From this point on all example code will have variable type identifiers spelled out.
Degrees and Radians
Vectors are perfect for setting objects in motion and pointing an object toward a known point but what if you want your object to travel at a precise 34.5 degree angle? Which vector quantity values would you need to use to achieve this? The answer lies with incorporating radian and degree values found around the perimeter of a circle.
A circle contains radian points on its perimeter ranging in value from 0 to 6.2831852, or 0 to 2 times the value of Pi (3.1415926). A radian is a direct relationship between the radius of the circle and its arc. If you want to read more about that relationship you can visit this page. All you need to know to incorporate this knowledge into game programming is that these values exist as shown in Figure 4 below.
Figure 4: Radians, degrees, and the associated conversion formulas
The points around a circle's perimeter can also be expressed in degrees also shown in Figure 4 above. The two formulas needed to convert between the two systems are given as well. QB64 makes the conversion process simple as it offers two statements to perform the calculations for you.
The _D2R and _R2D Statements
The _D2R statement takes a given degree value from 0 to 359.9999 and converts it to the equivalent radian:
Radian! = _D2R(Degree!) ' convert degree to radian
and the _R2D statement takes a given radian value from 0 to 6.2831852 and converts it the equivalent degree:
Degree! = _R2D(Radian!) ' convert radian to degree
Degrees and Radians to Vector
Once you have either a desired degree or radian heading you can then convert that value to a vector as shown in Figure 5 below.
Figure 5: Radian and degree to vector conversion
Radians can be converted directly to vector quantities using the SIN() (sine) and COS() (cosine) functions. Degrees will need to be converted to radians first as shown in the DEGREE TO VECTOR conversion in Figure 5. The degree to radian conversion formula seen in Figure 4 is embedded within the SIN() and COS() functions. Using _D2R the conversions would look like this:
Vx! = SIN(_D2R(Degree!)) ' calculate the x vector quantity
Vy! = -COS(_D2R(Degree!)) ' calculate the y vector quantity
This lesson isn't going to go into any discussion about sine and cosine and how they relate to radians. Again, all you need to know for game programming is that these functions are used to convert radian and degree points around the perimeter of a circle to vector quantities. If you wish to read more about sine and cosine you can visit this web site.
Ok, enough with the formulas for now. Let's put this information to practical use with an example program. Use the RIGHT and LEFT ARROW keys to rotate the space ship. Use the UP ARROW key to move the ship in the direction it is facing. The following program named RadianShipDemo.BAS can be found in your .\tutorial\Lesson17\ directory.
Figure 5: A ship made with radians and vectors (Asteroids)
This program allows you to envision the underlying imaginary circle that controls everything. The center of the ship is the center of the circle and the ship's vertices are simply radian values on that circle. The radian that creates the ship's nose point is used to calculate a vector quantity from. Which ever direction the nose is pointing is the direction the ship will travel because the vectors are calculated using this radian.
Lines 30 through 34 fill the Rad() array with 72 radian values spaced evenly 5 degrees apart from each other. Lines 35 through 37 identify the radian points on the circle where the ships lines are to be drawn creating the outline of the ship. Rad(0) is the front of the ship, Rad(28) is the lower right corner of the ship, and Rad(44) is the lower left corner of the ship. These three array index values are held in the Ship() array.
When the ship is rotated to the right these three values are increased by one. If one of the index values should happen to pass the highest index value of 71 it is reset back to 0 which now points to the beginning of the circle. Likewise when the ship is rotated to the left the three values are decreased by one. If one of the index values should happen to pass the lowest index value of 0 it is reset back to 71 which now points to the end of the circle. The left and right motion is handled in lines 55 through 62 within the FOR ... NEXT loop.
Also within the FOR ... NEXT loop in lines 48 through 63 each of three values has a vector calculated from it. Lines 49 and 50 use the radian value stored in the Rad() array to calculate a vector. Remember, vectors don't care about location, only a length and direction and these two lines of code calculate that for us.
The location is now added to these vectors in lines 64 through 67. The length and direction of each vector is multiplied by ShipSize and then added to the ship's x and y coordinates. ShipSize is nothing more than the radius of the imaginary circle that follows the ship around. This brings the three points out to the perimeter of the circle where the LINE command is used to "connect the dots" of the three points to draw the ship.
Lines 75 through 82 of the code control the motion of the ship. When the UP ARROW key is pressed lines 76 and 77 use the vector quantity information stored in Ship(1).Vx and Ship(1).Vy to calculate a new ship location. The Ship(1) vector quantities are related to the front of the ship so the ship moves in the current direction the ship is pointing.
This method of calculating vector quantities from radian points to draw lines on the screen is often referred to as Vector Graphics and was used in early arcade games such as Asteroids and Tempest. By using radians to point in a given direction and then converting that radian value to vector quantities you can now control in which direction an object points and moves without the need to reference another point as is required for vector math alone.
The example above works perfect for objects that fit neatly into a circle such as the ship did. With a bit of modification this method can also be used for irregular shapes that do not fit neatly into a circle. The next code example shows how irregular shaped objects can be stored in an array and then used later on to create the shape in any position, size, or rotation desired. The following source code can be found as RadianAsteroidDemo.BAS in your .\tutorial\Lesson17\ directory.
Figure 7: Beware, falling rocks ahead
Each point on the asteroids was created from a combination of a radian and radius length values. Figure 8 below describes this process.
Figure 8: Storing an irregular shaped object
The asteroid object x,y vector points are stored in line 88 of the code as a DATA statement. The FOR ... NEXT statement in lines 36 through 40 READs each set of values in and then converts them to a radius (length) and radian value. The next FOR ... NEXT statement in lines 41 through 47 sets up the Asteroids() array that contains each asteroid's location ( Asteroid().Loc.x/y ), the radian direction of travel ( Asteroid().Dir ), the asteroid speed ( Asteroid().Speed ), and finally the size of each asteroid ( Asteroid().Size ).
This information is then used in the FOR ... NEXT loop contained in lines 61 through 83. Lines 62 through 66 takes the first set of values in Object(1), converts the radian to a vector quantity, and then calculates the position on screen and saving the results in P1.x and P1.y. The PSET statement is used to draw a pixel at the location as a place-holder for the LINE statements that will follow.
The FOR ... NEXT loop in lines 67 through 73 cycle's through the remaining Object(2-10) values. Just like the first point these values are used to create the remainder of the screen positions. This time however a LINE statement is used to continue from the last screen position used and "connect the dots".
Line 74 completes the outer perimeter of the asteroid by joining the last point back to the first with a LINE statement.
Lines 75 through 78 then creates a motion vector using the radian value found Asteroid().Dir and adds those vector quantities to the x and y locations of the asteroid. Lines 79 through 82 detect if the asteroid is leaving the screen and if so wraps it around to the opposite side.
If the user presses the space bar to active asteroid rotation the variable Spin is set to TRUE which then activates the IF statement in line 55.
The FOR ... NEXT statement in lines 56 through 59 cycles through each Object()'s radian value and adds the value in SPINRATE to it, effectively causing the radian to move clockwise ever so slightly. Line 58 ensures that if the radian value exceeds the value of 2 * PI the value is wrapped back around to the beginning of the radian range by subtracting 2 * PI.
It's an amazingly small piece of code that achieves so much. To keep the code to a minimum for the tutorial purposes however a trade off was made. The spin of each asteroid can't be independently controlled since there is only one master Object() array to work with. To allow for independent spin rates and directions for each asteroid a separate Object() array for would be needed for each asteroid significantly increasing the size of the code. This will also allow for a greater variety of asteroids instead of just the one depicted on screen but for display and learning purposes this example code gets the point across very well.
A Trigonometry Visual Aid
Some people learn best by reading and doing, others through lectures, and still others through visual instruction (like the author). To this end the following program was created for the tutorial to help those who may benefit from seeing the actual math and graphics in motion in real time. The source code to the program TrigDemo.BAS can be found in your .\tutorial\Lesson17\ directory. You'll also need to make sure the files glinputtop.BI and glinput_noerror.BI are in the same directory as TrigDemo.BAS. The BI files are library add-ons needed for the program to run. (You'll learn about libraries in a later lesson.)
Figure 9: The relationship between radians, degrees, and vectors
Figure 10: Showing the relation between two objects
Figure 9 above shows the screen as it appears when you first run the program. The radian will sweep across the circle at first in an animated sequence. In the upper left hand corner you can enter a degree, radian, or vector and that graphical representation will appear within the circle along with all the math formulas filled out with the correct results.
Figure 10 above shows the second page. This page will allow you to enter two coordinate values and the relationships between them will be shown graphically within the grid window. The "from" coordinate is shown as the center of the circle and the "to" coordinate lies on the circle's perimeter. This relationship illustrates how the circle is still present as described on the first page.
This program also makes a handy little reference to have at your side to remember all the formulas and how to set them up.
The previous asteroid example isn't the only way to rotate a set of points around a common center. Another method is to use something called a rotation matrix. A rotation matrix is an advanced trig concept but basically it's used to perform a rotation in a given space. This lesson is not going to go into rotation matrix detail but if you're interested you can visit this page to read more.
The rotation matrix converts to these four lines of code:
SINr! = SIN(-Angle! / 57.2957795131) ' get the sine of the radian
COSr! = COS(-Angle! / 57.2957795131) ' get the cosine of the radian
Vx! = x! * COSr! + SINr! * y!) ' calculate new x vector
Vy! = y! * COSr! - x! * SINr!) ' calculate new y vector
x! and y! are the vectors where the point currently lies. Angle! is the degree in which to rotate the x! and y! vectors toward. The calculation -Angle! / 57.2957795131 in the first two lines are converting the angle degree into a radian. Finally, Vx! and Vy! are the new vector coordinates for the point. These four lines will only work when it's assumed that the center of rotation is coordinate (0, 0).
In the next code example this rotation matrix is used to rotate the four corners of an image. Once rotated the _MAPTRIANGLE command is used to map the original image onto a new image surface using the rotated Vx! and Vy! coordinate locations. The code named SpriteRotateDemo.BAS can be found in your .\tutorial\Lesson17\ directory.
Note: The four lines of code above were written by the original author of QB64, Rob (Galleon).
Figure 11: Rotating the bee image and setting it free
It's important to remember that a sprite's dimensions, the width and height, will change as it transitions through the rotation. If you replace line 38 with this code:
_PUTIMAGE (Bee.x, Bee.y), RotImg ' image not centered
you'll be able to see the size differences as the image will appear to wobble as it is drawn. You always need to use the center point of a sprite you wish to rotate to ensure the rotation looks correct. Lines 38 and 57 correct this:
_PUTIMAGE(Bee.x - _WIDTH(RotImg) /2, Bee.y - _HEIGHT(RotImg) / 2), RotImg
by moving the image to the left half of it's width and up half of it's height to ensure that Bee.x and Bee.y are truly the center point of the sprite.
For the best possible sprite rotations it's always best to use sprite images that have widths and heights that are not divisible by 2 (odd numbers). Sprites with these dimensions will have a true center point to work with and rotate as smoothly as possible.
In the RotateImage subroutine this centering of the sprite also has to be taken into account. Lines 105 through 114 retrieves the width and height of the image passed in. Four vector pairs are then created, px(0),py(0) through px(3),py(3), that create an area equal to the size of the image that has the coordinate (0,0) as the center point. The rotation matrix algorithm needs the center point to be (0,0) to rotate the four vector quantities around the center.
Lines 115 and 116 pre-calculate the sine and cosine of the radius used to rotate the four points. Lines 117 through 127 then loops through all four vector pairs which applies the matrix rotation algorithm them in lines 118 and 119. Lines 122 through 125 record the lowest and highest values for x and y coordinates seen. As stated earlier the size of the rotated sprite will change and lines 122 through 125 record this change.
Lines 128 and 129 then calculates the new rotated image width and height dimensions and are used to create the new image holder for the rotated image in line 138.
The center point however is still coordinate (0,0) so all vector points need to be shifted to the right and down half of the image width and height to put coordinate (0,0) back into the upper left corner of the image. This is performed in lines 133 through 137.
Finally the original image needs to be mapped onto the new image using the new vector quantities as the four points for the image. The _MAPTRIANGLE statement is used to do this. Figure 12 below shows this process.
Figure 12: Using _MAPTRIANGLE
_MAPTRIANGLE is used to grab any three coordinate points of an image and then map the resulting triangular image to any three subsequent points. It can even handle 3D drawing. Check out the _MAPTRIANGLE Wiki page for more information and abilities of this statement.
As an added bonus since you know the degree angle of rotation for the sprite you can use that information to set a vector for sprite motion. Lines 52 through 56 takes the degree angle and converts it to a radian. Vector quantities are then created from the radian value and finally added to the bee's location multiplied by a desired speed. You can now set your rotated sprites free.
Here's some code that puts everything covered in this lesson into use. The code named ZombieRotate.BAS can be found in your .\tutorial\Lesson17\ directory.
Figure 13: They're coming to get you Barbara!
This example makes use of sprites, a sprite sheet, sounds, and animation. Looks like a good start to a Zombie stalker game. Move the brain around on the screen with the mouse and watch the Zombie hungerly chase it. Again, it's amazing how little code is required to create something like this.