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 quite a few forum members that have impressive math skills and I have called upon their expertise many times. There are no stupid questions!
Vectors
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
DO
CIRCLE (x, y), 30 ' draw circle
x = x + 5 ' move circle to the right 5 pixels
y = y - 5 ' move circle up 5 pixels
LOOP
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)
DO
CIRCLE (x, y), 30 ' draw circle
x = x + Vx ' move circle x vector direction
y = y + Vy ' move circle y vector direction
LOOP
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.
( This code can be found at .\tutorial\Lesson17\BallVectorDemo.bas )
'* Simple vector demo - Bouncing Ball
CONST SWIDTH = 640, SHEIGHT = 480 ' screen width and height
CONST BALLRADIUS = 30 ' ball's radius
DIM BallX! ' ball's x coordinate
DIM BallY! ' ball's y coordinate
DIM BallVectorX! ' ball's x vector
DIM BallVectorY! ' ball's y vector
SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) ' graphics screen
RANDOMIZE TIMER ' seed RND number generator
BallX! = SWIDTH / 2 ' ball x location (center)
BallY! = SHEIGHT / 2 ' ball y location (center)
BallVectorX! = (INT(RND * 5) + 1) * SGN(RND - RND) ' ball x vector (-5 to 5)
BallVectorY! = (INT(RND * 5) + 1) * SGN(RND - RND) ' ball y vector (-5 to 5)
DO ' begin main loop
_LIMIT 60 ' 60 frames per second
CLS ' clear screen
BallX! = BallX! + BallVectorX! ' add vector to ball's x coordinate
BallY! = BallY! + BallVectorY! ' add vector to ball's y coordinate
IF BallX! >= SWIDTH - BALLRADIUS OR BallX! <= BALLRADIUS THEN ' did ball hit right/left side?
BallVectorX! = -BallVectorX! ' yes, reverse x vector
END IF
IF BallY! >= SHEIGHT - BALLRADIUS OR BallY! <= BALLRADIUS THEN ' did ball hit top/bottom side?
BallVectorY! = -BallVectorY! ' yes, reverse y vector
END IF
CIRCLE (BallX!, BallY!), BALLRADIUS ' draw ball
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
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 code can be found at .\tutorial\Lesson17\BallSpeedDemo.bas )
'* Simple vector demo - Bouncing Ball
CONST SWIDTH = 640, SHEIGHT = 480 ' screen width and height
CONST BALLRADIUS = 30 ' ball's radius
DIM BallX! ' ball's x coordinate
DIM BallY! ' ball's y coordinate
DIM Vx! ' ball's x vector
DIM Vy! ' ball's y vector
DIM Length! ' length of vector
DIM Speed% ' speed of ball
SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) ' graphics screen
RANDOMIZE TIMER ' seed RND number generator
BallX! = SWIDTH / 2 ' ball x location (center)
BallY! = SHEIGHT / 2 ' ball y location (center)
Speed% = 5 ' initial ball speed
Vx! = (INT(RND * 5) + 1) * SGN(RND - RND) ' ball x vector (-5 to 5)
Vy! = (INT(RND * 5) + 1) * SGN(RND - RND) ' ball y vector (-5 to 5)
Length! = SQR(Vx! * Vx! + Vy! * Vy!) ' length of vector
Vx! = Vx! / Length! ' normalize vector
Vy! = Vy! / Length!
DO ' begin main loop
_LIMIT 60 ' 60 frames per second
CLS ' clear screen
PRINT ' instructions
PRINT " Press right arrow key to increase speed, left arrow to decrease."
LOCATE 3, 35: PRINT "Speed ="; Speed% ' speed indicator
IF _KEYDOWN(19712) THEN Speed% = Speed% + 1 ' increase speed when right arrow pressed
IF _KEYDOWN(19200) THEN Speed% = Speed% - 1 ' decrease speed when left arrow pressed
IF Speed% < 1 THEN Speed% = 1 ' obey the speed limit
IF Speed% > 20 THEN Speed% = 20
BallX! = BallX! + Vx! * Speed% ' add vector to ball's x coordinate
BallY! = BallY! + Vy! * Speed% ' add vector to ball's y coordinate
IF BallX! >= SWIDTH - BALLRADIUS OR BallX! <= BALLRADIUS THEN ' did ball hit right/left side?
Vx! = -Vx! ' yes, reverse x vector
END IF
IF BallY! >= SHEIGHT - BALLRADIUS OR BallY! <= BALLRADIUS THEN ' did ball hit top/bottom side?
Vy! = -Vy! ' yes, reverse y vector
END IF
CIRCLE (BallX!, BallY!), BALLRADIUS ' draw ball
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
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.
Smart Vectors
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 at .\tutorial\Lesson17\TagVectorDemo.bas )
'* Vector Demo - Tag, you're it!
TYPE XYPAIR ' 2D point definition
x AS SINGLE ' x coordinate
y AS SINGLE ' y coordinate
END TYPE
TYPE A_CIRCLE ' circle definition
loc AS XYPAIR ' x,y coordinate location
vec AS XYPAIR ' x,y vectors
r AS INTEGER ' radius
END TYPE
DIM RCir AS A_CIRCLE ' a red circle
DIM GCir AS A_CIRCLE ' a green circle
DIM Speed AS INTEGER ' speed of red circle
SCREEN _NEWIMAGE(640, 480, 32) ' graphics screen
_MOUSEHIDE ' hide operating system mouse pointer
RCir.loc.x = 319 ' define circle properties
RCir.loc.y = 239
RCir.r = 30
GCir.r = 30
Speed = 3
DO ' begin main program loop
_LIMIT 60 ' 60 frames per second
CLS
WHILE _MOUSEINPUT: WEND ' get latest mouse information
GCir.loc.x = _MOUSEX ' update player location
GCir.loc.y = _MOUSEY
CIRCLE (GCir.loc.x, GCir.loc.y), GCir.r, _RGB32(0, 255, 0) ' draw player
PAINT (GCir.loc.x, GCir.loc.y), _RGB32(0, 127, 0), _RGB32(0, 255, 0)
P2PVector RCir.loc, GCir.loc, RCir.vec ' calculate enemy vectors
CIRCLE (RCir.loc.x, RCir.loc.y), RCir.r, _RGB32(255, 0, 0) ' draw enemy
PAINT (RCir.loc.x, RCir.loc.y), _RGB32(127, 0, 0), _RGB32(255, 0, 0)
LINE (RCir.loc.x, RCir.loc.y)-_
(RCir.loc.x + RCir.vec.x * RCir.r, RCir.loc.y + RCir.vec.y * RCir.r) ' white line from center
IF P2PDistance(RCir.loc, GCir.loc) > GCir.r + RCir.r THEN ' are circles colliding?
RCir.loc.x = RCir.loc.x + RCir.vec.x * Speed ' no, update enemy location
RCir.loc.y = RCir.loc.y + RCir.vec.y * Speed
LOCATE 2, 39: PRINT "TAG" ' print title of game
ELSE ' yes, circles colliding
LOCATE 2, 35: PRINT "You're it!" ' print message to player
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
SUB P2PVector (P1 AS XYPAIR, P2 AS XYPAIR, V AS XYPAIR)
'** NOTE: V passed by reference is altered
'** Point to Point Vector Calculator
'** Returns x,y vectors from P1 to P2
' P1.x, P1.y = FROM coordinate (INPUT )
' P2.x, P2.y = TO coordinate (INPUT )
' V.x, V.y = normalized vectors to P2 (OUTPUT)
DIM D AS SINGLE ' distance between points
V.x = P2.x - P1.x ' horizontal distance ( side A )
V.y = P2.y - P1.y ' vertical distance ( side B )
D = _HYPOT(V.x, V.y) ' direct distance (hypotenuse)
IF D = 0 THEN EXIT SUB ' can't divide by 0
V.x = V.x / D ' normalize x vector ( -1 to 1 )
V.y = V.y / D ' normalize y vector ( -1 to 1 )
END SUB
'------------------------------------------------------------------------------------------------------------
FUNCTION P2PDistance (P1 AS XYPAIR, P2 AS XYPAIR)
'** Point to Point Distance Calculator
'** Returns the distance between P1 and P2
' P1.x, P1.y - FROM coordinate (INPUT)
' P2.x, P2.y - TO coordinate (INPUT)
' returns SQR((P2.x - P1.x)^2 + (P2.y - P1.y)^2) using QB64 _HYPOT() function
P2PDistance = _HYPOT(P2.x - P1.x, P2.y - P1.y) ' return direct distance (hypotenuse)
END FUNCTION
'------------------------------------------------------------------------------------------------------------
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 ' calculate enemy vectors
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 at .\tutorial\Lesson17\VectorShootDemo.bas )
'* Vector demo - Turret shooter
CONST FALSE = 0, TRUE = NOT FALSE ' boolean truth detectors
CONST SWIDTH = 640, SHEIGHT = 480 ' screen dimensions
CONST YELLOW = _RGB32(255, 255, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST DARKGREEN = _RGB32(0, 127, 0)
CONST BLACK = _RGB32(0, 0, 0)
CONST GRAY = _RGB32(64, 64, 64)
CONST RED = _RGB32(255, 0, 0)
CONST MAGENTA = _RGB32(255, 0, 255)
TYPE XYPAIR ' 2D point definition
x AS SINGLE ' x coordinate
y AS SINGLE ' y coordinate
END TYPE
TYPE BULLET ' bullet definition
loc AS XYPAIR ' location of bullet
vec AS XYPAIR ' vector of bullet
speed AS SINGLE ' bullet speed
col AS _UNSIGNED LONG ' bullet color
active AS INTEGER ' active status of bullet
END TYPE
REDIM Bullet(0) AS BULLET ' dynamic bullet array
DIM CrossHair AS XYPAIR ' crosshair coordinates
DIM Center AS XYPAIR ' center of screen coordinates
DIM Vector AS XYPAIR ' normalized vector from gun barrel to crosshair
DIM Barrel AS XYPAIR ' barrel coordinates
DIM ButtonHeld AS INTEGER ' left mouse button flag
DIM Index AS INTEGER ' index number of free spot in bullet array
DIM Speed AS SINGLE ' bullet speed
SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) ' graphics screen
_MOUSEHIDE ' hide operating system mouse pointer
Center.x = SWIDTH / 2 ' screen center coordinates
Center.y = SHEIGHT / 2
Speed = 3 ' initial bullet speed
DO ' begin main loop
_LIMIT 60 ' 60 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse update
CrossHair.x = _MOUSEX ' set crosshair coordinates
CrossHair.y = _MOUSEY
LINE (CrossHair.x - 10, CrossHair.y)-(CrossHair.x + 10, CrossHair.y) ' draw crosshair
LINE (CrossHair.x, CrossHair.y - 10)-(CrossHair.x, CrossHair.y + 10)
P2PVector Center, CrossHair, Vector ' get vector from center to crosshair
Barrel.x = Center.x + Vector.x * 18 ' calculate barrel location
Barrel.y = Center.y + Vector.y * 18
CIRCLE (Center.x, Center.y), 30, GREEN ' draw gun turret
PAINT (Center.x, Center.y), DARKGREEN, GREEN
CIRCLE (Barrel.x, Barrel.y), 10, BLACK ' draw gun barrel
PAINT (Barrel.x, Barrel.y), GRAY, BLACK
PRINT " Power" ' draw power meter
LINE (10, 16)-(81, 31), YELLOW, B
LOCATE 3, 4: PRINT USING "###%"; ((Speed - 3) / 7) * 100
IF _MOUSEBUTTON(1) THEN ' left mouse button down?
ButtonHeld = TRUE ' yes, remember this
IF Speed < 10 THEN Speed = Speed + .25 ' increase speed while button down
LINE (11, 17)-((Speed - 2) * 10, 30), _RGB32(64 + Speed * 19, 0, 0), BF ' update power meter
END IF
IF NOT _MOUSEBUTTON(1) AND ButtonHeld THEN ' was left button released?
ButtonHeld = FALSE ' yes, remember this
Index = FreeIndex ' get free index in bullet array
Bullet(Index).active = TRUE ' mark bullet as active
Bullet(Index).loc = Barrel ' location of bullet
Bullet(Index).vec = Vector ' vector of bullet
Bullet(Index).speed = Speed ' speed of bullet
Bullet(Index).col = _RGB32(64 + Speed * 19, 0, 0) ' color of bullet match meter
Speed = 3 ' reset bullet speed
END IF
DrawBullets ' draw active bullets to screen
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
SUB DrawBullets ()
'** Draws all active bullets to the screen and maintains bullet array
SHARED Bullet() AS BULLET ' bullet array
DIM Count AS INTEGER ' array counter
DIM Clean AS INTEGER ' if true then no active bullets
Count = -1 ' reset array counter
Clean = TRUE ' assume no active bullets
DO ' begin bullet draw loop
Count = Count + 1 ' increment counter
IF Bullet(Count).active THEN ' bullet active?
Bullet(Count).loc.x = Bullet(Count).loc.x + Bullet(Count).vec.x * Bullet(Count).speed 'update bullet
Bullet(Count).loc.y = Bullet(Count).loc.y + Bullet(Count).vec.y * Bullet(Count).speed
CIRCLE (Bullet(Count).loc.x, Bullet(Count).loc.y), 10, MAGENTA ' draw bullet
PAINT (Bullet(Count).loc.x, Bullet(Count).loc.y), Bullet(Count).col, MAGENTA
CIRCLE (Bullet(Count).loc.x, Bullet(Count).loc.y), 10, RED
IF Bullet(Count).loc.x < -5 OR Bullet(Count).loc.x > SWIDTH + 5 THEN ' bullet leave left/right?
Bullet(Count).active = FALSE ' yes, bullet now inactive
END IF
IF Bullet(Count).loc.y < -5 OR Bullet(Count).loc.y > SHEIGHT + 5 THEN ' bullet leave up/down?
Bullet(Count).active = FALSE ' yes, bulllet now inactive
END IF
Clean = FALSE ' active bullets exist
END IF
LOOP UNTIL Count = UBOUND(Bullet) ' leave when all checked
IF Clean THEN REDIM Bullet(0) AS BULLET ' reset array if no bullets
END SUB
'------------------------------------------------------------------------------------------------------------
FUNCTION FreeIndex ()
'** Finds or creates a free index within the bullet array
SHARED Bullet() AS BULLET ' bullet array
DIM Count AS INTEGER ' array counter
DIM Index AS INTEGER ' free array index
Count = -1 ' set array counter
DO ' begin array search loop
Count = Count + 1 ' increment counter
IF NOT Bullet(Count).active THEN Index = Count ' use this index if bullet inactive
LOOP UNTIL (Count = UBOUND(Bullet)) OR Index ' leave when index found or full count
IF NOT Index THEN ' was a free index in the array found?
REDIM _PRESERVE Bullet(UBOUND(Bullet) + 1) AS BULLET ' no, increase array size by one
Index = UBOUND(Bullet) ' use new index for next bullet
END IF
FreeIndex = Index ' return free array index
END FUNCTION
'------------------------------------------------------------------------------------------------------------
SUB P2PVector (P1 AS XYPAIR, P2 AS XYPAIR, V AS XYPAIR)
'** NOTE: V passed by reference is altered
'** Point to Point Vector Calculator
'** Returns x,y vectors from P1 to P2
' P1.x, P1.y = FROM coordinate (INPUT )
' P2.x, P2.y = TO coordinate (INPUT )
' V.x, V.y = normalized vectors to P2 (OUTPUT)
DIM D AS SINGLE ' distance between points
V.x = P2.x - P1.x ' horizontal distance ( side A )
V.y = P2.y - P1.y ' vertical distance ( side B )
D = _HYPOT(V.x, V.y) ' direct distance (hypotenuse)
IF D = 0 THEN EXIT SUB ' can't divide by 0
V.x = V.x / D ' normalize x vector ( -1 to 1 )
V.y = V.y / D ' normalize y vector ( -1 to 1 )
END SUB
'------------------------------------------------------------------------------------------------------------
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%
DIM Count AS INTEGER
As well as these:
DIM KeyPress$
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:
TYPE XYPAIR
x AS INTEGER
y AS SINGLE
END TYPE
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.
Vector Graphics
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.
( This code can be found at .\tutorial\Lesson17\RadianShipDemo.bas )
'** Radian Ship Demo
CONST FALSE = 0, TRUE = NOT FALSE ' truth detectors
CONST PI = 3.1415926, PI2 = PI * 2 ' useful PI values
CONST ONEDEG = PI2 / 360 ' one radian degree
TYPE XYPAIR ' x,y value definition
x AS SINGLE ' x value
y AS SINGLE ' y value
END TYPE
TYPE SHIP ' ship point definition
Vx AS SINGLE ' point's x vector
Vy AS SINGLE ' point's y vector
rad AS INTEGER ' radian array index point sits at
END TYPE
DIM Ship(3) AS SHIP ' radian and vector quantities of each ship point
DIM ShipLoc AS XYPAIR ' ship's location on screen
DIM ShipSpeed AS INTEGER ' ship's speed
DIM ShipSize AS INTEGER ' ship's size
DIM Vector AS XYPAIR ' vector locations for hidden/showing radian circle
DIM Rad(72) AS SINGLE ' 72 radian points 5 degrees apart
DIM Radian AS SINGLE ' radian counter
DIM Count AS INTEGER ' generic counter
DIM LeftArrow AS INTEGER ' left arrow key toggle flag
DIM RightArrow AS INTEGER ' right arrow key toggle flag
DIM ShowRads AS INTEGER ' show radian circle toggle flag
Count = 0 ' store radian points in array
FOR Radian = 0 TO PI2 STEP ONEDEG * 5 ' 72 radian points 5 degrees apart
Rad(Count) = Radian ' save radian value
Count = Count + 1 ' increment counter
NEXT Radian
Ship(1).rad = 0 ' ship's nose cone pointing toward radian 0 or Rad(0)
Ship(2).rad = 28 ' ship's right side pointing toward radian .4886921 or Rad(28)
Ship(3).rad = 44 ' ship's left side pointing toward radian .7679448 or Rad(44)
ShipLoc.x = 319 ' ship's x coordinate on screen
ShipLoc.y = 239 ' ship's y coordinate on screen
ShipSpeed = 5 ' ship's speed
ShipSize = 60 ' ship's size
SCREEN _NEWIMAGE(640, 480, 32) ' graphics screen
DO ' begin main loop
_LIMIT 60 ' 60 frames per second
CLS ' clear screen
LOCATE 2, 11: PRINT "Right/Left Arrow keys to turn, Up Arrow key to move forward"; ' print directions
LOCATE 3, 25: PRINT "Spacebar to toggle radian circle"
FOR Count = 1 TO 3 ' cycle through 3 ship points
Ship(Count).Vx = SIN(Rad(Ship(Count).rad)) ' calculate vector of radian
Ship(Count).Vy = -COS(Rad(Ship(Count).rad))
IF ShowRads THEN ' draw radian lines if active
LINE (ShipLoc.x, ShipLoc.y)_
-(ShipLoc.x + Ship(Count).Vx * ShipSize * 2, ShipLoc.y + Ship(Count).Vy * ShipSize * 2)
END IF
IF LeftArrow THEN ' was left arrow key pressed?
Ship(Count).rad = Ship(Count).rad - 1 ' yes, update point index location
IF Ship(Count).rad < 0 THEN Ship(Count).rad = 71 ' reset to last index if needed
END IF
IF RightArrow THEN ' was left arrow key pressed?
Ship(Count).rad = Ship(Count).rad + 1 ' yes, update point index location
IF Ship(Count).rad > 71 THEN Ship(Count).rad = 0 ' reset to first index if needed
END IF
NEXT Count
LINE (ShipLoc.x + Ship(1).Vx * SHIPSIZE, ShipLoc.y + Ship(1).Vy * SHIPSIZE)_
-(ShipLoc.x + Ship(2).Vx * SHIPSIZE, ShipLoc.y + Ship(2).Vy * SHIPSIZE) ' draw the ship
LINE -(ShipLoc.x + Ship(3).Vx * ShipSize, ShipLoc.y + Ship(3).Vy * ShipSize)
LINE -(ShipLoc.x + Ship(1).Vx * ShipSize, ShipLoc.y + Ship(1).Vy * ShipSize)
IF ShowRads THEN ' did user select to show radians?
FOR Count = 0 TO 71 ' yes, cycle through radian array
Vector.x = SIN(Rad(Count)) ' calculate vector of radian
Vector.y = -COS(Rad(Count))
PSET (ShipLoc.x + Vector.x * ShipSize, ShipLoc.y + Vector.y * ShipSize) ' draw pixel at radian point
NEXT Count
END IF
IF _KEYDOWN(18432) THEN ' up arrow key pressed?
ShipLoc.x = ShipLoc.x + Ship(1).Vx * ShipSpeed ' yes, calculate vector of ship
ShipLoc.y = ShipLoc.y + Ship(1).Vy * ShipSpeed
IF ShipLoc.x < -ShipSize / 2 THEN ShipLoc.x = 640 + ShipSize / 2 ' keep ship on screen
IF ShipLoc.x > 640 + ShipSize / 2 THEN ShipLoc.x = -ShipSize / 2
IF ShipLoc.y < -ShipSize / 2 THEN ShipLoc.y = 480 + ShipSize / 2
IF ShipLoc.y > 480 + ShipSize / 2 THEN ShipLoc.y = -ShipSize / 2
END IF
IF _KEYDOWN(19200) THEN LeftArrow = TRUE ELSE LeftArrow = FALSE ' toggle flag when left pressed
IF _KEYDOWN(19712) THEN RightArrow = TRUE ELSE RightArrow = FALSE ' toggle flag when right pressed
IF _KEYHIT = 32 THEN ShowRads = NOT ShowRads ' toggle flag when space bar pressed
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
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.
( This code can be found at .\tutorial\Lesson17\RadianAsteroidDemo.bas )
'** Asteroid demo using radians
CONST FALSE = 0, TRUE = NOT FALSE ' truth detectors
CONST PI = 3.1415926, PI2 = 2 * PI ' useful PI values
CONST SPINRATE = PI2 / 360 ' asteroid spin rate
CONST SWIDTH = 640, SHEIGHT = 480 ' screen dimensions
CONST MAXASTEROIDS = 20 ' number of asteroids on screen
TYPE XYPAIR ' 2D point location definition
x AS SINGLE ' x coordinate
y AS SINGLE ' y coordinate
END TYPE
TYPE OBJECT ' object definition (points that make up an object)
Radian AS SINGLE ' direction of point
Radius AS SINGLE ' distance to point from center
END TYPE
TYPE ASTEROID ' asteroid definition
Loc AS XYPAIR ' asteroid location
Dir AS SINGLE ' asteroid radian direction
Speed AS INTEGER ' asteroid speed
Size AS INTEGER ' asteroid size
END TYPE
DIM Object(10) AS OBJECT ' object point data
DIM Asteroid(MAXASTEROIDS) AS ASTEROID ' asteroids array
DIM Vector AS XYPAIR ' vector calculations
DIM Obj AS INTEGER ' object counter
DIM Ast AS INTEGER ' asteroid counter
DIM P1 AS XYPAIR ' first object point for PSET
DIM Np AS XYPAIR ' next object point for LINE
DIM Spin AS INTEGER ' TRUE to activate spin, FALSE otherwise
RANDOMIZE TIMER ' seed RND generator
FOR Obj = 1 TO 10 ' cycle through object points
READ Vector.x, Vector.y ' get object x,y vector point
Object(Obj).Radius = _HYPOT(Vector.x, Vector.y) ' calculate radius from vector
Object(Obj).Radian = _ATAN2(Vector.x, Vector.y) ' calculate direction from vector
NEXT Obj
FOR Ast = 1 TO MAXASTEROIDS ' cycle through asteroids
Asteroid(Ast).Loc.x = INT(RND * (SWIDTH - 40)) + 20 ' random location
Asteroid(Ast).Loc.y = INT(RND * (SHEIGHT - 40)) + 20
Asteroid(Ast).Dir = RND * PI2 ' random direction
Asteroid(Ast).Speed = INT(RND * 6) + 2 ' random speed
Asteroid(Ast).Size = 2 ^ INT(RND * 3) ' random size
NEXT Ast
SCREEN _NEWIMAGE(SWIDTH, SHEIGHT, 32) ' graphics screen
Spin = FALSE ' no asteroid spin
DO ' begin main loop
CLS ' clear screen
_LIMIT 60 ' 60 frames per second
LOCATE 2, 19: PRINT "Press the spacebar to activate asteroid spin" ' print directions
IF _KEYHIT = 32 THEN Spin = NOT Spin ' flip spin flag if spacebar pressed
IF Spin THEN ' spin asteroids?
FOR Obj = 1 TO 10 ' yes, cycle through object points
Object(Obj).Radian = Object(Obj).Radian + SPINRATE ' move radian location
IF Object(Obj).Radian > PI2 THEN Object(Obj).Radian = Object(Obj).Radian - PI2 ' keep within limits
NEXT Obj
END IF
FOR Ast = 1 TO MAXASTEROIDS ' cycle through asteroids
Vector.x = SIN(Object(1).Radian) ' calculate vector from 1st object point
Vector.y = COS(Object(1).Radian)
P1.x = Asteroid(Ast).Loc.x + Vector.x * Object(1).Radius * Asteroid(Ast).Size ' plot location on screen
P1.y = Asteroid(Ast).Loc.y + Vector.y * Object(1).Radius * Asteroid(Ast).Size
PSET (P1.x, P1.y) ' draw a pixel
FOR Obj = 2 TO 10 ' cycle through remaining points
Vector.x = SIN(Object(Obj).Radian) ' calculate vector from object point
Vector.y = COS(Object(Obj).Radian)
Np.x = Asteroid(Ast).Loc.x + Vector.x * Object(Obj).Radius * Asteroid(Ast).Size ' plot location
Np.y = Asteroid(Ast).Loc.y + Vector.y * Object(Obj).Radius * Asteroid(Ast).Size ' on screen
LINE -(Np.x, Np.y) ' draw line from previous point
NEXT Obj
LINE -(P1.x, P1.y) ' draw final line back to start
Vector.x = SIN(Asteroid(Ast).Dir) ' get vector from asteroid radian
Vector.y = COS(Asteroid(Ast).Dir)
Asteroid(Ast).Loc.x = Asteroid(Ast).Loc.x + Vector.x * Asteroid(Ast).Speed ' plot location on screen
Asteroid(Ast).Loc.y = Asteroid(Ast).Loc.y + Vector.y * Asteroid(Ast).Speed
IF Asteroid(Ast).Loc.x < -19 THEN Asteroid(Ast).Loc.x = SWIDTH + 19 ' keep asteroids on screen
IF Asteroid(Ast).Loc.x > SWIDTH + 19 THEN Asteroid(Ast).Loc.x = -19
IF Asteroid(Ast).Loc.y < -19 THEN Asteroid(Ast).Loc.y = SHEIGHT + 19
IF Asteroid(Ast).Loc.y > SHEIGHT + 19 THEN Asteroid(Ast).Loc.y = -19
NEXT Ast
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
' Asteroid object data (10 coordinate vector points)
DATA 0,-5,5,-10,10,-5,7,0,10,5,2,10,-5,10,-10,5,-10,-5,-5,-10
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 lesson 20.)
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.
Sprite Rotation
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.
Note: The four lines of code above were written by the original author of QB64, Rob (Galleon).
( This code can be found at .\tutorial\Lesson17\SpriteRotateDemo.bas )
'** Sprite Rotation Demo
CONST FALSE = 0, TRUE = NOT FALSE ' truth detectors
CONST SWIDTH = 640, SHEIGHT = 480 ' screen width and height
CONST GRAY = _RGB32(64, 64, 64) ' define colors
CONST WHITE = _RGB32(255, 255, 255)
TYPE XYPAIR ' x,y pair definition
x AS SINGLE ' x value
y AS SINGLE ' y value
END TYPE
DIM Img AS LONG ' original bee image
DIM RotImg AS LONG ' rotated image of bee
DIM Angle AS INTEGER ' current angle of rotation (degrees)
DIM FlyBeFree AS INTEGER ' TRUE if be is flying around
DIM Speed AS INTEGER ' speed of bee in flight
DIM Bee AS XYPAIR ' bee image x,y flight coordinates
DIM Vector AS XYPAIR ' bee image x,y vector values in flight
DIM SpinDir AS INTEGER ' rotation direction of image
SCREEN _NEWIMAGE(640, 480, 32) ' graphics screen
COLOR WHITE, GRAY ' white text on gray background
Img = _LOADIMAGE(".\tutorial\Lesson13\tbee0.png", 32) ' load bee image
Speed = 2 ' set bee flight speed
DO ' begin main loop
IF NOT FlyBeFree THEN ' is bee free to fly?
Bee.x = 319 ' center bee on screen
Bee.y = 239
DO ' begin stationary bee loop
_LIMIT 120 ' 120 frames per second
CLS , GRAY ' clear screen with gray background
LOCATE 2, 22: PRINT "Press space bar to set the bee free." ' display instructions
IF _KEYHIT = 32 THEN FlyBeFree = TRUE ' if space bar pressed set flag
LOCATE 5, 9: PRINT "Original Image"; ' no, text above original image
_PUTIMAGE (50, 50), Img ' display original image
RotateImage Angle, Img, RotImg ' rotate bee image
_PUTIMAGE (Bee.x - _WIDTH(RotImg) / 2, Bee.y - _HEIGHT(RotImg) / 2), RotImg ' display bee
LOCATE 21, 26: PRINT "Rotating one degree per frame." ' display info
Angle = Angle + 1 ' increment rotation angle (degrees)
IF Angle = 360 THEN Angle = 0 ' reset angle when needed
_DISPLAY ' update screen with changes
LOOP UNTIL FlyBeFree OR _KEYDOWN(27) ' leave when bee freed or ESC pressed
ELSE ' yes, bee is free to fly
Angle = 0 ' reset angle
SpinDir = 1 ' clockwise spin (positive degrees)
DO ' begin flight loop
_LIMIT 120 ' 120 frames per second
CLS , GRAY ' clear screen with gray background
LOCATE 2, 24: PRINT "Press space bar to trap the bee." ' display instructions
RotateImage Angle, Img, RotImg ' rotate bee image
Radian = _D2R(Angle) ' convert degree angle to radian
Vector.x = SIN(Radian) ' calculate vectors from radian
Vector.y = -COS(Radian)
Bee.x = Bee.x + Vector.x * Speed ' update bee location
Bee.y = Bee.y + Vector.y * Speed
_PUTIMAGE (Bee.x - _WIDTH(RotImg) / 2, Bee.y - _HEIGHT(RotImg) / 2), RotImg ' display bee
Angle = Angle + SpinDir ' increment angle (degrees)
IF ABS(Angle) = 360 THEN ' full circle reached?
SpinDir = -SpinDir ' yes, reverse rotation
Angle = 0 ' reset angle
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYHIT = 32 OR _KEYDOWN(27) ' leave when space bar or ESC pressed
FlyBeFree = FALSE ' reset flag
Angle = 0 ' reset angle
END IF
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
SUB RotateImage (Degree AS SINGLE, InImg AS LONG, OutImg AS LONG)
'** Rotates an image by the degree specified.
'
'NOTE: OutImg passed by reference is altered
'
'Degree - amount of rotation to add to image (-359.999.. - +359.999...) (INPUT)
'InImg - image to rotate (INPUT )
'OutImg - rotated image (OUTPUT)
'
'** This subroutine based on code provided by Rob (Galleon) on the QB64.NET website in 2009.
'** Special thanks to Luke for explaining the matrix rotation formula used in this routine.
DIM px(3) AS INTEGER ' x vector values of four corners of image
DIM py(3) AS INTEGER ' y vector values of four corners of image
DIM Left AS INTEGER ' left-most value seen when calculating rotated image size
DIM Right AS INTEGER ' right-most value seen when calculating rotated image size
DIM Top AS INTEGER ' top-most value seen when calculating rotated image size
DIM Bottom AS INTEGER ' bottom-most value seen when calculating rotated image size
DIM RotWidth AS INTEGER ' width of rotated image
DIM RotHeight AS INTEGER ' height of rotated image
DIM WInImg AS INTEGER ' width of original image
DIM HInImg AS INTEGER ' height of original image
DIM Xoffset AS INTEGER ' offsets used to move (0,0) back to upper left corner of image
DIM Yoffset AS INTEGER
DIM COSr AS SINGLE ' cosine of radian calculation for matrix rotation
DIM SINr AS SINGLE ' sine of radian calculation for matrix rotation
DIM x AS SINGLE ' new x vector of rotated point
DIM y AS SINGLE ' new y vector of rotated point
DIM v AS INTEGER ' vector counter
IF OutImg THEN _FREEIMAGE OutImg ' free any existing image
WInImg = _WIDTH(InImg) ' width of original image
HInImg = _HEIGHT(InImg) ' height of original image
px(0) = -WInImg / 2 ' -x,-y ------------------- x,-y
py(0) = -HInImg / 2 ' Create points around (0,0) px(0),py(0) | | px(3),py(3)
px(1) = px(0) ' that match the size of the | |
py(1) = HInImg / 2 ' original image. This | . |
px(2) = WInImg / 2 ' creates four vector | 0,0 |
py(2) = py(1) ' quantities to work with. | |
px(3) = px(2) ' px(1),py(1) | | px(2),py(2)
py(3) = py(0) ' -x,y ------------------- x,y
SINr = SIN(-Degree / 57.2957795131) ' sine and cosine calculation for rotation matrix below
COSr = COS(-Degree / 57.2957795131) ' degree converted to radian, -Degree for clockwise rotation
DO ' cycle through vectors
x = px(v) * COSr + SINr * py(v) ' perform 2D rotation matrix on vector
y = py(v) * COSr - px(v) * SINr ' https://en.wikipedia.org/wiki/Rotation_matrix
px(v) = x ' save new x vector
py(v) = y ' save new y vector
IF px(v) < Left THEN Left = px(v) ' keep track of new rotated image size
IF px(v) > Right THEN Right = px(v)
IF py(v) < Top THEN Top = py(v)
IF py(v) > Bottom THEN Bottom = py(v)
v = v + 1 ' increment vector counter
LOOP UNTIL v = 4 ' leave when all vectors processed
RotWidth = Right - Left + 1 ' calculate width of rotated image
RotHeight = Bottom - Top + 1 ' calculate height of rotated image
Xoffset = RotWidth \ 2 ' place (0,0) in upper left corner of rotated image
Yoffset = RotHeight \ 2
v = 0 ' reset corner counter
DO ' cycle through rotated image coordinates
px(v) = px(v) + Xoffset ' move image coordinates so (0,0) in upper left corner
py(v) = py(v) + Yoffset
v = v + 1 ' increment corner counter
LOOP UNTIL v = 4 ' leave when all four corners of image moved
OutImg = _NEWIMAGE(RotWidth, RotHeight, 32) ' create rotated image canvas
' map triangles onto new rotated image canvas
_MAPTRIANGLE (0, 0)-(0, HInImg - 1)-(WInImg - 1, HInImg - 1), InImg TO _
(px(0), py(0))-(px(1), py(1))-(px(2), py(2)), OutImg
_MAPTRIANGLE (0, 0)-(WInImg - 1, 0)-(WInImg - 1, HInImg - 1), InImg TO _
(px(0), py(0))-(px(3), py(3))-(px(2), py(2)), OutImg
END SUB
'------------------------------------------------------------------------------------------------------------
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 ' display bee
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.
( This code can be found at .\tutorial\Lesson17\ZombieRotate.bas )
'** Demo Zombie Animation With Rotation
'** Overhead zombie sprites provided by Riley Gombart at:
'** https://opengameart.org/content/animated-top-down-zombie
'** Sounds downloaded from The Sounds Resource at:
'** https://www.sounds-resource.com/pc_computer/plantsvszombies/sound/1430
TYPE XYPAIR ' x,y pair definition
x AS SINGLE ' x value
y AS SINGLE ' y value
END TYPE
CONST TRANSPARENT = _RGB32(255, 0, 255) ' transparent color
DIM ZombieSheet AS LONG ' zombie sprite sheet
DIM Zombie(15) AS LONG ' zombie sprites parsed from sheet
DIM Brains AS LONG ' brain image
DIM Groan(6) AS LONG ' zombie groaning sounds
DIM RotatedZombie AS LONG ' rotated zombie sprite
DIM Sprite AS INTEGER ' sprite counter
DIM Angle2Brains AS SINGLE ' angle from zombie to brains
DIM Frame AS INTEGER ' frame counter
DIM Zomby AS XYPAIR ' zombie location
DIM Brain AS XYPAIR ' brain location
DIM Vector AS XYPAIR ' vector to brains
'** Load sprite sheet and parse out individual sprites and load zombie groan sounds
ZombieSheet = _LOADIMAGE(".\tutorial\Lesson17\zombie311x288.png", 32)
_CLEARCOLOR TRANSPARENT, ZombieSheet
FOR Sprite = 0 TO 15 '
Zombie(Sprite) = _NEWIMAGE(311, 288, 32)
_PUTIMAGE , ZombieSheet, Zombie(Sprite), (Sprite * 311, 0)-(Sprite * 311 + 310, 287)
IF Sprite < 6 THEN
Groan(Sprite + 1) = _SNDOPEN(".\tutorial\Lesson17\groan" + _TRIM$(STR$(Sprite + 1)) + ".ogg")
END IF
NEXT Sprite
_FREEIMAGE ZombieSheet ' sprite sheet no longer needed
Brains = _LOADIMAGE(".\tutorial\Lesson17\brains.png", 32) ' load brain image
_CLEARCOLOR TRANSPARENT, Brains ' set brain image transparent color
SCREEN _NEWIMAGE(800, 600, 32) ' enter graphics screen
RANDOMIZE TIMER ' seed random number generator
_MOUSEHIDE ' hide mouse pointer
Zomby.x = 399 ' position zombie
Zomby.y = 299
Sprite = 0 ' reset sprite counter
DO ' begin main loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
Brain.x = _MOUSEX ' record mouse location
Brain.y = _MOUSEY
Angle2Brains = P2PDegree(Zomby, Brain) ' get angle from zombie to brains
Degree2Vector Angle2Brains, Vector ' convert angle to vector
Frame = Frame + 1 ' increment frame counter
IF Frame = 90 THEN ' 90 frames gone by? (3 seconds)
Frame = 0 ' yes, reset frame counter
_SNDPLAY Groan(INT(RND(1) * 6) + 1) ' play random zombie groan
END IF
IF Frame MOD 5 = 0 THEN ' frame evenly divisible by 5? (6 FPS)
Sprite = Sprite + 1 ' yes, increment to next sprite in animation
IF Sprite = 16 THEN Sprite = 0 ' keep sprite number within limit
RotateImage Angle2Brains, Zombie(Sprite), RotatedZombie ' rotate zombie sprite at same angle
END IF
Zomby.x = Zomby.x + Vector.x ' update zombie location
Zomby.y = Zomby.y + Vector.y ' draw centered zombie and brains
_PUTIMAGE (Zomby.x - _WIDTH(RotatedZombie) \ 2, Zomby.y - _HEIGHT(RotatedZombie) \ 2), RotatedZombie
_PUTIMAGE (Brain.x - _WIDTH(Brains) \ 2, Brain.y - _HEIGHT(Brains) \ 2), Brains
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
FOR Sprite = 0 TO 15 ' cycle through zombie images (memory cleanup)
_FREEIMAGE Zombie(Sprite) ' remove image from memory
IF Sprite < 6 THEN _SNDCLOSE Groan(Sprite + 1) ' remove sound from memory
NEXT Sprite
_FREEIMAGE RotatedZombie ' remove image from memory
_FREEIMAGE Brains ' remove image from memory
SYSTEM ' return to operating system
------------------------------------------------------------------------------------------------------------
SUB RotateImage (Degree AS SINGLE, InImg AS LONG, OutImg AS LONG)
'** Rotates an image by the degree specified.
'NOTE: OutImg passed by reference is altered
'Degree - amount of rotation to add to image (-359.999.. - +359.999...) (INPUT)
'InImg - image to rotate (INPUT )
'OutImg - rotated image (OUTPUT)
'** This subroutine based on code provided by Rob (Galleon) on the QB64.NET website in 2009.
'** Special thanks to Luke for explaining the matrix rotation formula used in this routine.
DIM px(3) AS INTEGER ' x vector values of four corners of image
DIM py(3) AS INTEGER ' y vector values of four corners of image
DIM Left AS INTEGER ' left-most value seen when calculating rotated image size
DIM Right AS INTEGER ' right-most value seen when calculating rotated image size
DIM Top AS INTEGER ' top-most value seen when calculating rotated image size
DIM Bottom AS INTEGER ' bottom-most value seen when calculating rotated image size
DIM RotWidth AS INTEGER ' width of rotated image
DIM RotHeight AS INTEGER ' height of rotated image
DIM WInImg AS INTEGER ' width of original image
DIM HInImg AS INTEGER ' height of original image
DIM Xoffset AS INTEGER ' offsets used to move (0,0) back to upper left corner of image
DIM Yoffset AS INTEGER
DIM COSr AS SINGLE ' cosine of radian calculation for matrix rotation
DIM SINr AS SINGLE ' sine of radian calculation for matrix rotation
DIM x AS SINGLE ' new x vector of rotated point
DIM y AS SINGLE ' new y vector of rotated point
DIM v AS INTEGER ' vector counter
IF OutImg THEN _FREEIMAGE OutImg ' free any existing image
WInImg = _WIDTH(InImg) ' width of original image
HInImg = _HEIGHT(InImg) ' height of original image
px(0) = -WInImg / 2 ' -x,-y ------------------- x,-y
py(0) = -HInImg / 2 ' Create points around (0,0) px(0),py(0) | | px(3),py(3)
px(1) = px(0) ' that match the size of the | |
py(1) = HInImg / 2 ' original image. This | . |
px(2) = WInImg / 2 ' creates four vector | 0,0 |
py(2) = py(1) ' quantities to work with. | |
px(3) = px(2) ' px(1),py(1) | | px(2),py(2)
py(3) = py(0) ' -x,y ------------------- x,y
SINr = SIN(-Degree / 57.2957795131) ' sine and cosine calculation for rotation matrix below
COSr = COS(-Degree / 57.2957795131) ' degree converted to radian, -Degree for clockwise rotation
DO ' cycle through vectors
x = px(v) * COSr + SINr * py(v) ' perform 2D rotation matrix on vector
y = py(v) * COSr - px(v) * SINr ' https://en.wikipedia.org/wiki/Rotation_matrix
px(v) = x ' save new x vector
py(v) = y ' save new y vector
IF px(v) < Left THEN Left = px(v) ' keep track of new rotated image size
IF px(v) > Right THEN Right = px(v)
IF py(v) < Top THEN Top = py(v)
IF py(v) > Bottom THEN Bottom = py(v)
v = v + 1 ' increment vector counter
LOOP UNTIL v = 4 ' leave when all vectors processed
RotWidth = Right - Left + 1 ' calculate width of rotated image
RotHeight = Bottom - Top + 1 ' calculate height of rotated image
Xoffset = RotWidth \ 2 ' place (0,0) in upper left corner of rotated image
Yoffset = RotHeight \ 2
v = 0 ' reset corner counter
DO ' cycle through rotated image coordinates
px(v) = px(v) + Xoffset ' move image coordinates so (0,0) in upper left corner
py(v) = py(v) + Yoffset
v = v + 1 ' increment corner counter
LOOP UNTIL v = 4 ' leave when all four corners of image moved
OutImg = _NEWIMAGE(RotWidth, RotHeight, 32) ' create rotated image canvas
' map triangles onto new rotated image canvas
_MAPTRIANGLE (0, 0)-(0, HInImg - 1)-(WInImg - 1, HInImg - 1), InImg TO _
(px(0), py(0))-(px(1), py(1))-(px(2), py(2)), OutImg
_MAPTRIANGLE (0, 0)-(WInImg - 1, 0)-(WInImg - 1, HInImg - 1), InImg TO _
(px(0), py(0))-(px(3), py(3))-(px(2), py(2)), OutImg
END SUB
'------------------------------------------------------------------------------------------------------------
SUB Degree2Vector (D AS SINGLE, V AS XYPAIR)
'** NOTE: V passed by reference is altered
'** Degree to Vector Calculator
'** Converts the supplied degree to a normalized vector
' D - degree (INPUT )
' V.x, V.y - normalized vector (OUTPUT)
' .017453292 = PI / 180
DIM Degree AS SINGLE ' the degree value passed in
Degree = D ' don't alter passed in value
IF Degree < 0 OR Degree >= 360 THEN ' degree outside limits?
Degree = FixRange(Degree, 360) ' yes, correct degree
END IF
V.x = SIN(Degree * .017453292) ' return x vector
V.y = -COS(Degree * .017453292) ' return y vector
END SUB
'------------------------------------------------------------------------------------------------------------
FUNCTION P2PDegree (P1 AS XYPAIR, P2 AS XYPAIR)
'** Point to Point Degree Calculator
'** Returns the degree from point 1 to point 2 with 0 degrees being up
' P1.x, P1.y - FROM coordinate (INPUT)
' P2.x, P2.y - TO coordinate (INPUT)
' 57.29578 = 180 / PI
DIM Theta AS SINGLE ' the returned degree Degree
IF P1.y = P2.y THEN ' do both points have same y value?
IF P1.x = P2.x THEN ' yes, do both points have same x value?
EXIT FUNCTION ' yes, identical points, no degree (0)
END IF
IF P2.x > P1.x THEN ' is second point to the right of first point?
P2PDegree = 90 ' yes, the degree must be 90
ELSE ' no, second point is to the left of first point
P2PDegree = 270 ' the degree must be 270
END IF
EXIT FUNCTION ' leave function, degree calculated
END IF
IF P1.x = P2.x THEN ' do both points have the same x value?
IF P2.y > P1.y THEN ' yes, is second point below first point?
P2PDegree = 180 ' yes, the degree must be 180
END IF
EXIT FUNCTION ' leave function, degree calculated
END IF
Theta = _ATAN2(P2.y - P1.y, P2.x - P1.x) * 57.29578 ' calculate +/-180 degree Degree
IF Theta < 0 THEN Theta = 360 + Theta ' convert to 360 degree
Theta = Theta + 90 ' set 0 degrees as up
IF Theta > 360 THEN Theta = Theta - 360 ' adjust accordingly if needed
P2PDegree = Theta ' return degree (0 to 359.99..)
END FUNCTION
'------------------------------------------------------------------------------------------------------------
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.