Lesson 15: Collision Detection
From this point on the remaining lessons will focus on game programming techniques instead of being tutorial based. Lessons 1 through 14 introduced you to many of the commands needed to get a start in game programming with QB64. It's time to start putting those commands into usable code suitable for making games. The first and most basic concept that most games implement is collision detection. This lesson will investigate four different methods of collision detection; rectangular, circular, pixel perfect, and line intersection.
Rectangular Based Collision Detection
The simplest form of collision detection is if two rectangular areas are overlapping. Many of the earliest games purposely used sprite images that fit into rectangular areas such as 16x16, 32x32, 64x64, or a combination of these such as 32x64. Mario Brothers is a perfect example of this. All sprite images on the screen were created to fit within a 16x16 pixel area and the sprite images themselves were made to take up as much of that area as possible. Figure 1 below shows the Mario Brothers screen divided into a 16x16 grid allowing you to readily see how everything, including the pipe images, platforms, and even the score, fit neatly into this grid.
Note: The original Mario Brothers screen was 256x224 pixels. It has been resized 300% in Figure 1 for clarity.
Figure 1: The Mario Brothers screen divided into a 16x16 pixel grid
Only three pieces of information are required from each rectangle to test for rectangular collision; the upper left x,y coordinate, the width, and the height. This made collision detection in early games such as Mario Brothers very easy to achieve. The following code illustrates the use of collision detection between two rectangular objects. Use the mouse to move the red box around on the screen. When it touches the green box a collision is registered.
( This code can be found at .\tutorial\Lesson15\CollisionDemo1.bas )
'** Rectangular Collision Demo
CONST RED = _RGB32(255, 0, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST YELLOW = _RGB32(255, 255, 0)
DIM RedX%, RedY% ' red box upper left coordinate
DIM GreenX%, GreenY% ' green box upper left coordinate
DIM RedWidth%, RedHeight% ' red box width and height
DIM GreenWidth%, GreenHeight% ' green box width and height
DIM BoxColor~& ' color of green box
GreenX% = 294 ' upper left X coordinate of green box
GreenY% = 214 ' upper left Y coordinate of green box
RedWidth% = 25 ' width of red box
RedHeight% = 25 ' height of red box
GreenWidth% = 50 ' width of green box
GreenHeight% = 50 ' height of green box
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide the mouse pointer
DO ' begin main program loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
RedX% = _MOUSEX ' record mouse X location
RedY% = _MOUSEY ' record mouse Y location
'** check for collision between two rectangular areas
IF RectCollide(RedX%, RedY%, RedWidth%, RedHeight%, GreenX%, GreenY%, GreenWidth%, GreenHeight%) THEN
LOCATE 2, 36 ' position text cursor
PRINT "COLLISION!" ' report collision happening
BoxColor~& = YELLOW ' green box will become yellow during collision
ELSE ' no collision
BoxColor~& = GREEN ' green box will be green when no collision
END IF
'** draw the two rectangles to screen
LINE (GreenX%, GreenY%)-(GreenX% + GreenWidth% - 1, GreenY% + GreenHeight% - 1), BoxColor~&, BF
LINE (RedX%, RedY%)-(RedX% + RedWidth% - 1, RedY% + RedHeight% - 1), RED, BF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
FUNCTION RectCollide (Rectangle1_x1%, Rectangle1_y1%, Rectangle1Width%, Rectangle1Height%,_
Rectangle2_x1%, Rectangle2_y1%, Rectangle2Width%, Rectangle2Height%)
'--------------------------------------------------------------------------------------------------------
'- Checks for the collision between two rectangular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- Rectangle1_x1% - rectangle 1 upper left X -
'- Rectangle1_y1% - rectangle 1 upper left y -
'- Rectangle1Width% - width of rectangle 1 -
'- Rectangle1Height% - height of rectangle 1 -
'- Rectangle2_x1% - rectangle 2 upper left X -
'- Rectangle2_y1% - rectangle 2 upper left Y -
'- Rectangle2Width% - width of rectangle 2 -
'- Rectangle2Height% - height of rectangle 2 -
'-----------------------------------------------------------
'** declare local variables
DIM Rectangle1_x2% ' rectangle 1 lower right X
DIM Rectangle1_y2% ' rectangle 1 lower right Y
DIM Rectangle2_x2% ' rectangle 2 lower right X
DIM Rectangle2_y2% ' rectangle 2 lower right Y
'** calculate lower right X,Y coordinate for each rectangle
Rectangle1_x2% = Rectangle1_x1% + Rectangle1Width% - 1 ' rectangle 1 lower right X
Rectangle1_y2% = Rectangle1_y1% + Rectangle1Height% - 1 ' rectangle 1 lower right Y
Rectangle2_x2% = Rectangle2_x1% + Rectangle2Width% - 1 ' rectangle 2 lower right X
Rectangle2_y2% = Rectangle2_y1% + Rectangle2Height% - 1 ' rectangle 2 lower right Y
'** test for collision
RectCollide = 0 ' assume no collision
IF Rectangle1_x2% >= Rectangle2_x1% THEN ' rect 1 lower right X >= rect 2 upper left X ?
IF Rectangle1_x1% <= Rectangle2_x2% THEN ' rect 1 upper left X <= rect 2 lower right X ?
IF Rectangle1_y2% >= Rectangle2_y1% THEN ' rect 1 lower right Y >= rect 2 upper left Y ?
IF Rectangle1_y1% <= Rectangle2_y2% THEN ' rect 1 upper left Y <= rect 2 lower right Y ?
RectCollide = -1 ' if all 4 IFs true then a collision must be happening
END IF
END IF
END IF
END IF
END FUNCTION
Figure 2: Colliding rectangles
This example has been highly over coded for readability. Don't let its size intimidate you. The function RectCollide() accepts the upper left x,y coordinate of each rectangle as well the width and height of each one. From there it calculates the lower right x,y coordinate for each rectangle and then performs the collision check. Figures 3 and 4 below show a simplified version of how the collision check is performed using simple IF statements. If all of the IF statements equate to true then there must be a collision. If any of the IF statements equate to false then there can be no collision.
Figure 3: Conditions met for collision
Figure 4: Conditions not met for collision
When writing a game you are going to be testing for collisions between multiple images flying around the screen. You are almost always going to use a function to accomplish this as the example above does. There are a number of different ways to achieve this. Using the upper left x,y coordinate, width, and height of each rectangular area, as shown in the previous example, is just one way to test for rectangular collision. Another method, as shown in the next example, is to pass the upper left x,y and lower right x,y coordinates of each rectangular area. With this method the function does not have to calculate the lower right x,y coordinates of each rectangle.
( This code can be found at .\tutorial\Lesson15\CollisionDemo2.bas )
'** Rectangular Collision Demo #2
CONST RED = _RGB32(255, 0, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST YELLOW = _RGB32(255, 255, 0)
TYPE RECT ' rectangle definition
x1 AS INTEGER ' upper left X
y1 AS INTEGER ' upper left Y
x2 AS INTEGER ' lower right X
y2 AS INTEGER ' lower right Y
END TYPE
DIM RedBox AS RECT ' red rectangle coordinates
DIM GreenBox AS RECT ' green rectangle coordinates
DIM BoxColor~& ' color of green box
GreenBox.x1 = 294 ' upper left X coordinate of green box
GreenBox.y1 = 214 ' upper left Y coordinate of green box
GreenBox.x2 = 344 ' lower right X coordinate of green box
GreenBox.y2 = 264 ' lower right Y coordinate of green box
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide the mouse pointer
DO ' begin main program loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
RedBox.x1 = _MOUSEX ' record mouse X location
RedBox.y1 = _MOUSEY ' record mouse Y location
RedBox.x2 = RedBox.x1 + 25 ' calculate lower right X
RedBox.y2 = RedBox.y1 + 25 ' calculate lower right Y
IF RectCollide(RedBox, GreenBox) THEN ' rectangle collision?
LOCATE 2, 36 ' yes, position text cursor
PRINT "COLLISION!" ' report collision happening
BoxColor~& = YELLOW ' green box will become yellow during collision
ELSE ' no collision
BoxColor~& = GREEN ' green box will be green when no collision
END IF
LINE (GreenBox.x1, GreenBox.y1)-(GreenBox.x2, GreenBox.y2), BoxColor~&, BF ' draw green box
LINE (RedBox.x1, RedBox.y1)-(RedBox.x2, RedBox.y2), RED, BF ' draw red box
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
FUNCTION RectCollide (Rect1 AS RECT, Rect2 AS RECT)
'--------------------------------------------------------------------------------------------------------
'- Checks for the collision between two rectangular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- Rect1 - rectangle 1 coordinates -
'- Rect2 - rectangle 2 coordinates -
'-----------------------------------------------------------
RectCollide = 0 ' assume no collision
IF Rect1.x2 >= Rect2.x1 THEN ' rect 1 lower right X >= rect 2 upper left X ?
IF Rect1.x1 <= Rect2.x2 THEN ' rect 1 upper left X <= rect 2 lower right X ?
IF Rect1.y2 >= Rect2.y1 THEN ' rect 1 lower right Y >= rect 2 upper left Y ?
IF Rect1.y1 <= Rect2.y2 THEN ' rect 1 upper left Y <= rect 2 lower right Y ?
RectCollide = -1 ' if all 4 IFs true then a collision must be happening
END IF
END IF
END IF
END IF
END FUNCTION
In the second example a TYPE definition is used to define a rectangle structure and that TYPE is passed to the RectCollide() function. Since both the upper left x,y and lower right x,y coordinates are contained in the TYPE definition the function has no conversions to do first. The output of the second example is exactly the same as the first as seen in Figure 2 above.
Note: If the way TYPE definitions are being used here to transfer data between the main code body and function is not clear to you read the section on line intersection collision detection for a full explanation.
Circular Based Collision Detection
Games involving circular objects, such as pool balls or asteroids, will benefit from circular collision detection since rectangular detection would be woefully inadequate. Circular collision detection involves a bit of math, the Pythagorean Theorem to be exact, and because of this is a slightly slower method of collision detection. With circular collision detection two pieces of information are needed from the objects being tested; an x,y center point of the object and the radius to extend the detection out to. The following example program illustrates how circular detection is achieved. Use the mouse to move the red circle around on the screen.
( This code can be found at .\tutorial\Lesson15\CollisionDemo3.bas )
'** Circular Collision Demo #3
CONST RED = _RGB32(255, 0, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST YELLOW = _RGB32(255, 255, 0)
TYPE CIRCLES ' circle definition
x AS INTEGER ' center X of circle
y AS INTEGER ' center Y of circle
radius AS INTEGER ' circle radius
END TYPE
DIM RedCircle AS CIRCLES ' red circle properties
DIM GreenCircle AS CIRCLES ' green circle properties
DIM CircleColor~& ' color of green circle
GreenCircle.x = 319 ' green circle center X
GreenCircle.y = 239 ' green circle center Y
GreenCircle.radius = 50 ' green circle radius
RedCircle.radius = 25 ' red circle radius
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide the mouse pointer
DO ' begin main program loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
RedCircle.x = _MOUSEX ' record mouse X location
RedCircle.y = _MOUSEY ' record mouse Y location
IF CircCollide(RedCircle, GreenCircle) THEN ' circle collision?
LOCATE 2, 36 ' yes, position text cursor
PRINT "COLLISION!" ' report collision happening
CircleColor~& = YELLOW ' green circle become yellow during collision
ELSE ' no collision
CircleColor~& = GREEN ' green circle will be green when no collision
END IF
CIRCLE (GreenCircle.x, GreenCircle.y), GreenCircle.radius, CircleColor~& ' draw green circle
PAINT (GreenCircle.x, GreenCircle.y), CircleColor~&, CircleColor~& ' paint green circle
CIRCLE (RedCircle.x, RedCircle.y), RedCircle.radius, RED ' draw red circle
PAINT (RedCircle.x, RedCircle.y), RED, RED ' paint red circle
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
FUNCTION CircCollide (Circ1 AS CIRCLES, Circ2 AS CIRCLES)
'--------------------------------------------------------------------------------------------------------
'- Checks for the collision between two circular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- Circ1 - circle 1 properties -
'- Circ2 - circle 2 properties -
'- -
'- Removal of square root by Brandon Ritchie for -
'- more efficient and faster code. 05/10/20 -
'- -
'- original code read: -
'- -
'- Hypot% = INT(SQR(SideA% * SideA% + SideB% * SideB%)) -
'- IF Hypot% <= Circ1.radius + Circ2.radius THEN CircCollide = -1 -
'- -
'- Changed to current code below -
'-------------------------------------------------------------------
'** declare local variables
DIM SideA% ' side A length of right triangle
DIM SideB% ' side B length of right triangle
DIM Hypot& ' hypotenuse squared length of right triangle (side C)
'** check for collision
CircCollide = 0 ' assume no collision
SideA% = Circ1.x - Circ2.x ' calculate length of side A
SideB% = Circ1.y - Circ2.y ' calculate length of side B
Hypot& = SideA% * SideA% + SideB% * SideB% ' calculate hypotenuse squared
'** is hypotenuse squared <= the square of radii added together?
'** if so then collision has occurred
IF Hypot& <= (Circ1.radius + Circ2.radius) * (Circ1.radius + Circ2.radius) THEN CircCollide = -1
END FUNCTION
Figure 5: Colliding circles
Collision detection is achieved by setting up a right triangle between the center point of the two objects. The Pythagorean Theorem A2 + B2 = C2 can then be applied to determine the distance between the two object center points. Side A is the difference between x1 and x2 and side B is the difference between y1 and y2. Plugging the values for A and B into the equation yields the length of side C, or the distance between the two center points of the circles. If side C is less then or equal to the two radii of the objects added together then a collision must be occurring. Figure 6 below is a visual representation of this.
Figure 6: Pythagoras to the rescue
Because determining the square root of a number is labor intensive on the CPU many detection algorithms that require circular collision detection will first detect for a rectangular collision. Since rectangular collision requires only basic math and the use of IF statements it can be used first to test for two objects being in close proximity. Only when the objects are close enough does it warrant a circular collision detection check. The previous example has been modified to check for rectangular collision before determining if a circular collision has occurred.
Update: While updating the tutorial a few years back my son asked why I was using SQR in my circular collision detection routine. He reworked the math to remove the SQR statement all together which did in fact make the circular detection code faster. He's quite the coder himself and taught me something new!
( This code can be found at .\tutorial\Lesson15\CollisionDemo4.bas )
'** Circular Collision Demo #4
'** Rectangular Collision Checked First
CONST RED = _RGB32(255, 0, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST YELLOW = _RGB32(255, 255, 0)
CONST DARKGREEN = _RGB32(0, 127, 0)
TYPE CIRCLES ' circle definition
x AS INTEGER ' center X of circle
y AS INTEGER ' center Y of circle
radius AS INTEGER ' circle radius
END TYPE
TYPE RECT ' rectangle definition
x1 AS INTEGER ' upper left X
y1 AS INTEGER ' upper left Y
x2 AS INTEGER ' lower right X
y2 AS INTEGER ' lower right Y
END TYPE
DIM RedCircle AS CIRCLES ' red circle properties
DIM GreenCircle AS CIRCLES ' green circle properties
DIM RedBox AS RECT ' rectangle coordinates of red circle
DIM GreenBox AS RECT ' rectangle coordinates of green circle
DIM CircleColor~& ' color of green circle
GreenCircle.x = 319 ' green circle center X
GreenCircle.y = 239 ' green circle center Y
GreenCircle.radius = 50 ' green circle radius
RedCircle.radius = 25 ' red circle radius
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide the mouse pointer
DO ' begin main program loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
RedCircle.x = _MOUSEX ' record mouse X location
RedCircle.y = _MOUSEY ' record mouse Y location
RedBox.x1 = RedCircle.x - RedCircle.radius ' calculate rectangular coordinates
RedBox.y1 = RedCircle.y - RedCircle.radius ' for red and green circle
RedBox.x2 = RedCircle.x + RedCircle.radius
RedBox.y2 = RedCircle.y + RedCircle.radius
GreenBox.x1 = GreenCircle.x - GreenCircle.radius
GreenBox.y1 = GreenCircle.y - GreenCircle.radius
GreenBox.x2 = GreenCircle.x + GreenCircle.radius
GreenBox.y2 = GreenCircle.y + GreenCircle.radius
IF RectCollide(RedBox, GreenBox) THEN ' rectangular collision?
LOCATE 2, 33 ' yes, position text cursor
PRINT "PROXIMITY ALERT!" ' report the close proximity
CircleColor~& = DARKGREEN ' green circle become dark green during proximity
IF CircCollide(RedCircle, GreenCircle) THEN ' circle collision?
LOCATE 2, 33 ' yes, position text cursor
PRINT " COLLISION! " ' report collision happening
CircleColor~& = YELLOW ' green circle become yellow during collision
END IF
ELSE ' no collision
CircleColor~& = GREEN ' green circle will be green when no collision
END IF
CIRCLE (GreenCircle.x, GreenCircle.y), GreenCircle.radius, CircleColor~& ' draw green circle
PAINT (GreenCircle.x, GreenCircle.y), CircleColor~&, CircleColor~& ' paint green circle
CIRCLE (RedCircle.x, RedCircle.y), RedCircle.radius, RED ' draw red circle
PAINT (RedCircle.x, RedCircle.y), RED, RED ' paint red circle
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
FUNCTION RectCollide (Rect1 AS RECT, Rect2 AS RECT)
'--------------------------------------------------------------------------------------------------------
'- Checks for the collision between two rectangular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- Rect1 - rectangle 1 coordinates -
'- Rect2 - rectangle 2 coordinates -
'-----------------------------------------------------------
RectCollide = 0 ' assume no collision
IF Rect1.x2 >= Rect2.x1 THEN ' rect 1 lower right X >= rect 2 upper left X ?
IF Rect1.x1 <= Rect2.x2 THEN ' rect 1 upper left X <= rect 2 lower right X ?
IF Rect1.y2 >= Rect2.y1 THEN ' rect 1 lower right Y >= rect 2 upper left Y ?
IF Rect1.y1 <= Rect2.y2 THEN ' rect 1 upper left Y <= rect 2 lower right Y ?
RectCollide = -1 ' if all 4 IFs true then a collision must be happening
END IF
END IF
END IF
END IF
END FUNCTION
'------------------------------------------------------------------------------------------------------------
FUNCTION CircCollide (Circ1 AS CIRCLES, Circ2 AS CIRCLES)
'--------------------------------------------------------------------------------------------------------
'- Checks for the collision between two circular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- Circ1 - circle 1 properties -
'- Circ2 - circle 2 properties -
'- -
'- Removal of square root by Brandon Ritchie for -
'- more efficient and faster code. 05/10/20 -
'- -
'- original code read: -
'- -
'- Hypot% = INT(SQR(SideA% * SideA% + SideB% * SideB%)) -
'- IF Hypot% <= Circ1.radius + Circ2.radius THEN CircCollide = -1 -
'- -
'- Changed to current code below -
'-------------------------------------------------------------------
'** declare local variables
DIM SideA% ' side A length of right triangle
DIM SideB% ' side B length of right triangle
DIM Hypot& ' hypotenuse squared length of right triangle (side C)
'** check for collision
CircCollide = 0 ' assume no collision
SideA% = Circ1.x - Circ2.x ' calculate length of side A
SideB% = Circ1.y - Circ2.y ' calculate length of side B
Hypot& = SideA% * SideA% + SideB% * SideB% ' calculate hypotenuse squared
'** is hypotenuse squared <= the square of radii added together?
'** if so then collison has occurred
IF Hypot& <= (Circ1.radius + Circ2.radius) * (Circ1.radius + Circ2.radius) THEN CircCollide = -1
END FUNCTION
Figure 7: Rectangular and circular collision detection working together
Pixel Perfect Based Collision Detection
When absolute accuracy is needed for collision detection nothing can beat pixel perfect collision detection. It's one of the most CPU labor intensive methods of collision detection available however. Pixel based collision works by first identifying if a rectangular collision has occurred then the overlapping areas of the two rectangles are checked pixel by pixel for any overlapping pixels. Depending on the size of the overlapping area and the location of the pixel collision this method can vary wildly from check to check in time taken to complete the test. Pixel detection should only be used on images that absolutely need to incorporate its accuracy. The following example program loads two oval images and then checks for pixel collision between them. Use the mouse to move the red oval to see just how accurate it is. You'll need to copy the code to your qb64 folder before executing.
( This code can be found at .\tutorial\Lesson15\CollisionDemo5.bas )
'** Pixel Perfect Collision Demo #5
TYPE SPRITE ' sprite definition
image AS LONG ' sprite image
mask AS LONG ' sprite mask image
x1 AS INTEGER ' upper left X
y1 AS INTEGER ' upper left Y
x2 AS INTEGER ' lower right X
y2 AS INTEGER ' lower right Y
END TYPE
DIM RedOval AS SPRITE ' red oval images
DIM GreenOval AS SPRITE ' green oval images
RedOval.image = _LOADIMAGE(".\tutorial\Lesson15\redoval.png", 32) ' load red oval image image
GreenOval.image = _LOADIMAGE(".\tutorial\Lesson15\greenoval.png", 32) ' load green oval image
MakeMask RedOval ' create mask for red oval image
MakeMask GreenOval ' create mask for green oval image
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide the mouse pointer
GreenOval.x1 = 294 ' green oval upper left X
GreenOval.y1 = 165 ' green oval upper left Y
DO ' begin main program loop
_LIMIT 30 ' 30 frames per second
CLS ' clear screen
WHILE _MOUSEINPUT: WEND ' get latest mouse information
_PUTIMAGE (GreenOval.x1, GreenOval.y1), GreenOval.image ' display green oval
_PUTIMAGE (RedOval.x1, RedOval.y1), RedOval.image ' display red oval
RedOval.x1 = _MOUSEX ' record mouse X location
RedOval.y1 = _MOUSEY ' record mouse Y location
IF PixelCollide(GreenOval, RedOval) THEN ' pixel collision?
LOCATE 2, 36 ' yes, position text cursor
PRINT "COLLISION!" ' report collision happening
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
SUB MakeMask (Obj AS SPRITE)
'--------------------------------------------------------------------------------------------------------
'- Creates a negative mask of image for pixel collision detection. -
'- -
'- Obj - object containing an image and mask image holder -
'-------------------------------------------------------------------
DIM x%, y% ' image column and row counters
DIM cc~& ' clear transparent color
DIM Osource& ' original source image
DIM Odest& ' original destination image
Obj.mask = _NEWIMAGE(_WIDTH(Obj.image), _HEIGHT(Obj.image), 32) ' create mask image
Osource& = _SOURCE ' save source image
Odest& = _DEST ' save destination image
_SOURCE Obj.image ' make object image the source
_DEST Obj.mask ' make object mask image the destination
cc~& = _RGB32(255, 0, 255) ' set the color to be used as transparent
FOR y% = 0 TO _HEIGHT(Obj.image) - 1 ' cycle through image rows
FOR x% = 0 TO _WIDTH(Obj.image) - 1 ' cycle through image columns
IF POINT(x%, y%) = cc~& THEN ' is image pixel the transparent color?
PSET (x%, y%), _RGB32(0, 0, 0, 255) ' yes, set corresponding mask image to solid black
ELSE ' no, pixel is part of actual image
PSET (x%, y%), cc~& ' set corresponding mask image to transparent color
END IF
NEXT x%
NEXT y%
_DEST Odest& ' restore original destination image
_SOURCE Osource& ' restore original source image
_SETALPHA 0, cc~&, Obj.image ' set image transparent color
_SETALPHA 0, cc~&, Obj.mask ' set mask transparent color
END SUB
'------------------------------------------------------------------------------------------------------------
FUNCTION PixelCollide (Obj1 AS SPRITE, Obj2 AS SPRITE)
'--------------------------------------------------------------------------------------------------------
'- Checks for pixel perfect collision between two rectangular areas. -
'- Returns -1 if in collision -
'- Returns 0 if no collision -
'- -
'- obj1 - rectangle 1 coordinates -
'- obj2 - rectangle 2 coordinates -
'---------------------------------------------------------------------
DIM x1%, y1% ' upper left x,y coordinate of rectangular collision area
DIM x2%, y2% ' lower right x,y coordinate of rectangular collision area
DIM Test& ' overlap image to test for collision
DIM Hit% ' -1 (TRUE) if a collision occurs, 0 (FALSE) otherwise
DIM Osource& ' original source image handle
DIM p~& ' pixel color being tested in overlap image
Obj1.x2 = Obj1.x1 + _WIDTH(Obj1.image) - 1 ' calculate lower right x,y coordinates
Obj1.y2 = Obj1.y1 + _HEIGHT(Obj1.image) - 1 ' of both objects
Obj2.x2 = Obj2.x1 + _WIDTH(Obj2.image) - 1
Obj2.y2 = Obj2.y1 + _HEIGHT(Obj2.image) - 1
Hit% = 0 ' assume no collision
'** perform rectangular collision check
IF Obj1.x2 >= Obj2.x1 THEN ' rect 1 lower right X >= rect 2 upper left X ?
IF Obj1.x1 <= Obj2.x2 THEN ' rect 1 upper left X <= rect 2 lower right X ?
IF Obj1.y2 >= Obj2.y1 THEN ' rect 1 lower right Y >= rect 2 upper left Y ?
IF Obj1.y1 <= Obj2.y2 THEN ' rect 1 upper left Y <= rect 2 lower right Y ?
'** rectangular collision detected, perform pixel perfect collision check
IF Obj2.x1 <= Obj1.x1 THEN x1% = Obj1.x1 ELSE x1% = Obj2.x1 ' calculate overlapping
IF Obj2.y1 <= Obj1.y1 THEN y1% = Obj1.y1 ELSE y1% = Obj2.y1 ' square coordinates
IF Obj2.x2 <= Obj1.x2 THEN x2% = Obj2.x2 ELSE x2% = Obj1.x2
IF Obj2.y2 <= Obj1.y2 THEN y2% = Obj2.y2 ELSE y2% = Obj1.y2
Test& = _NEWIMAGE(x2% - x1% + 1, y2% - y1% + 1, 32) ' make overlap image
_PUTIMAGE (-(x1% - Obj1.x1), -(y1% - Obj1.y1)), Obj1.image, Test& ' place image 1
_PUTIMAGE (-(x1% - Obj2.x1), -(y1% - Obj2.y1)), Obj2.mask, Test& ' place image mask 2
'** enable the line below to see a visual representation of mask on image
'_PUTIMAGE (x1%, y1%), Test&
y1% = 0 ' reset row counter
Osource& = _SOURCE ' record current source image
_SOURCE Test& ' make test image the source
DO ' begin row (y) loop
x1% = 0 ' reset column counter
DO ' begin column (x) loop
p~& = POINT(x1%, y1%) ' get color at current coordinate
'** if color from object 1 then a collision has occurred
IF p~& <> _RGB32(0, 0, 0, 255) AND p~& <> _RGB32(0, 0, 0, 0) THEN Hit% = -1
x1% = x1% + 1 ' increment to next column
LOOP UNTIL x1% = _WIDTH(Test&) OR Hit% ' leave when column checked or collision
y1% = y1% + 1 ' increment to next row
LOOP UNTIL y1% = _HEIGHT(Test&) OR Hit% ' leave when all rows checked or collision
_SOURCE Osource& ' restore original destination
_FREEIMAGE Test& ' test image no longer needed (free RAM)
END IF
END IF
END IF
END IF
PixelCollide = Hit% ' return result of collision check
END FUNCTION
Figure 8: A pixel perfect collision has occurred
Pixel detection is achieved by using a negative image called an image mask. Every pixel in the original image that is not transparent is mirrored in the mask as a transparent pixel. Every pixel in the original image that is a transparent pixel is mirrored in the mask as a solid black pixel. When the mask is placed over a second image any pixels that show through indicate that a pixel collision must have occurred. If you activate line 116 in the example code above you'll see the mask image being applied to the green oval. Only when the green pixels appear through the mask is a pixel collision detected. Figure 9 below describes the process.
Figure 9: Applying masks to test for pixel collision detection
The MakeMask() subroutine creates a mask image by scanning the original image from top to bottom one pixel at a time. The pixels are mirrored from the original image to the mask image but converted using the method described in Figure 9.
Lines 100 through 103 in the PixelCollide() function perform a standard rectangular collision check. If the rectangles have merged then that overlapping area is identified by lines 107 through 110. x1%, y1%, x2%, and y2% now contain the coordinates of the overlapping area. Line 111 then creates an image the same size as the overlapping area.
Lines 112 and 113 place the original image and the mask of the second image onto the newly created overlap image. The location of the images are calculated in these two lines so as to overlap in the same manner as they will on the screen.
Lines 121 through 132 then cycle through every pixel contained in the overlap image using the POINT statement to grab each pixel's color. If line 128 detects a pixel that is not solid black or transparent then it must be a pixel from the image underneath and a collision is recorded by setting the variable Hit% to -1 (true). DO...LOOP statements are used here so they can easily be exited as soon as a collision is detected. Once a collision is detected there is no reason to check the remainder of the overlap image.
Line Intersection Collision Detection
Line intersection collision detection is used to detect if two line segments are crossing paths. I find this type of collision detection useful for games that emulate vector graphics like Asteroids or Tempest. These vector based games used lines to draw objects on the screen instead of sprite images. My Widescreen Asteroids game built with QB64 is an example that uses line intersection collision detection for all of the objects seen on the screen.
Line intersection collision is also useful for small and fast bullet collision detection. Many times when bullets are flying around on the screen they can "skip" over an image from one frame to the next. By projecting an imaginary line from the current bullet coordinate to the next predicted coordinate that line can be used to see if it will intersect the object through its flight path.
The following example program illustrates the use of line intersection to achieve collision detection. Use the mouse to move the red line around on the screen to intercept the rotating green line.
Note: The following line collision routine is new and uses a completely different method of detection which is faster and more efficient. If you are using the previous method found from the old tutorial web site I highly encourage you to incorporate this new method.
( This code can be found at .\tutorial\Lesson15\CollisionDemo6.bas )
'* Line Intersection Collision Detection Demo #6
'*
'* Note: This is a completely new method of line intersection detection from the tutorial's
'* previous code example. This detection algorithm is much smaller and more efficient.
'*
'* Updated 08/19/22
'*
'* Make note of the credit given in the LineCollide() subroutine.
CONST RED = _RGB32(255, 0, 0) ' define colors
CONST GREEN = _RGB32(0, 255, 0)
CONST YELLOW = _RGB32(255, 255, 0)
TYPE A_2D_POINT ' X,Y point definition
x AS INTEGER ' X coordinate location
y AS INTEGER ' Y coordinate location
END TYPE
TYPE A_2D_LINE ' line segment definition (notice a TYPE def can be used in another TYPE)
s AS A_2D_POINT ' start of line
e AS A_2D_POINT ' end of line
END TYPE
DIM Intersect AS A_2D_POINT ' X,Y line intersection coordinate identified by LineCollide()
DIM Line1 AS A_2D_LINE ' green line
DIM Line2 AS A_2D_LINE ' red line
DIM Plot(359) AS A_2D_POINT ' 360 degree plotted coordinates
DIM c! ' sine wave counter
DIM c1%, c2% ' counters
DIM clr~& ' green line color
FOR c1% = 0 TO 359 ' plot 360 points for spinning line
Plot(c1%).x = 319 + 100 * COS(c!) ' from screen center X radius 100
Plot(c1%).y = 239 + 100 * -SIN(c!) ' from screen center Y radius 100
c! = c! + 6.2831852 / 360 ' 360 increments
NEXT c1%
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
_MOUSEHIDE ' hide mouse pointer
c1% = 0 ' set green line start counter
c2% = 179 ' set green line end counter
DO ' begin main loop
_LIMIT 60 ' 30 frames per second
CLS ' clear the screen
LINE (Line1.s.x, Line1.s.y)-(Line1.e.x, Line1.e.y), clr~& ' draw green line
LINE (Line2.s.x, Line2.s.y)-(Line2.e.x, Line2.e.y), RED ' draw red line
WHILE _MOUSEINPUT: WEND ' get latest mouse information
Line2.s.x = _MOUSEX - 50 ' set red line coordinates
Line2.e.x = Line2.s.x + 100
Line2.s.y = _MOUSEY
Line2.e.y = Line2.s.y
Line1.s.x = Plot(c1%).x ' set green line coordinates
Line1.s.y = Plot(c1%).y
Line1.e.x = Plot(c2%).x
Line1.e.y = Plot(c2%).y
IF LineCollide%(Line2, Line1, Intersect) THEN ' line collision?
LOCATE 2, 36 ' yes, locate cursor
PRINT "COLLISION!" ' report collision
clr~& = YELLOW ' green line becomes yellow
CIRCLE (Intersect.x, Intersect.y), 4, GREEN
PAINT (Intersect.x, Intersect.y), GREEN, GREEN
ELSE ' no collision
clr~& = GREEN ' green line is green
END IF
c1% = c1% + 1 ' increment green line start counter
IF c1% = 360 THEN c1% = 0 ' keep within limits
c2% = c2% + 1 ' increment green line end counter
IF c2% = 360 THEN c2% = 0 ' keep within limits
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
'------------------------------------------------------------------------------------------------------------
FUNCTION LineCollide% (L1 AS A_2D_LINE, L2 AS A_2D_LINE, Intersect AS A_2D_POINT)
'Tests for 2 lines intersecting (in collision)
'Returns: 0 if no collision, -1 if collision
' : Intersect.x, Intersect.y will contain the x,y coordinate of collision
'L1.s.x start line 1 | L2.s.x start line 2 | Intersect.x line intersection coordinates
' .s.y | .s.y | .y
' .e.x end line 1 | .e.x end line 2 |
' .e.y | .e.y |
'This function created from examples given at this discussion:
'https://forum.unity.com/threads/line-intersection.17384/
'Based on the "Faster Line Segment Intersection" code found in Graphics Gems III
'pages 199-202 by Franklin Antonio (1992). ISBN 0-12-409672-7
DIM Ax%, Bx%, Cx%, Ay%, By%, Cy%, d%, e%, f%
DIM x1lo%, x1hi%, y1lo%, y1hi%
Intersect.x = 0 ' assume no collision
Intersect.y = 0
LineCollide% = 0
Ax% = L1.e.x - L1.s.x ' X bounding box test
Bx% = L2.s.x - L2.e.x
IF Ax% < 0 THEN
x1lo% = L1.e.x
x1hi% = L1.s.x
ELSE
x1hi% = L1.e.x
x1lo% = L1.s.x
END IF
IF Bx% > 0 THEN
IF x1hi% < L2.e.x OR L2.s.x < x1lo% THEN EXIT FUNCTION
ELSE
IF x1hi% < L2.s.x OR L2.e.x < x1lo% THEN EXIT FUNCTION
END IF
Ay% = L1.e.y - L1.s.y ' Y bounding box test
By% = L2.s.y - L2.e.y
IF Ay% < 0 THEN
y1lo% = L1.e.y
y1hi% = L1.s.y
ELSE
y1hi% = L1.e.y
y1lo% = L1.s.y
END IF
IF By% > 0 THEN
IF y1hi% < L2.e.y OR L2.s.y < y1lo% THEN EXIT FUNCTION
ELSE
IF y1hi% < L2.s.y OR L2.e.y < y1lo% THEN EXIT FUNCTION
END IF
Cx% = L1.s.x - L2.s.x
Cy% = L1.s.y - L2.s.y
d% = By% * Cx% - Bx% * Cy% ' alpha numerator
f% = Ay% * Bx% - Ax% * By% ' both denominators
IF f% = 0 THEN EXIT FUNCTION ' parallel line check
IF f% > 0 THEN ' alpha tests
IF d% < 0 OR d% > f% THEN EXIT FUNCTION
ELSE
IF d% > 0 OR d% < f% THEN EXIT FUNCTION
END IF
e% = Ax% * Cy% - Ay% * Cx% ' beta numerator
IF f% > 0 THEN ' beta tests
IF e% < 0 OR e% > f% THEN EXIT FUNCTION
ELSE
IF e% > 0 OR e% < f% THEN EXIT FUNCTION
END IF
Intersect.x = L1.s.x + d% * Ax% / f% ' calculate intersect coordinate
Intersect.y = L1.s.y + d% * Ay% / f%
LineCollide% = -1 ' report that collision occurred
END FUNCTION
Figure 10: Line intersect collision detected
I'm no mathematician and I'm sure line segment intersection was covered in the geometry class I took in the 10th grade back in 1983. The great thing about programming is somewhere, someone, has more than likely posted code or discussed the topic you are searching for. The code posted may even be in another programming language but the skills you learn in QB64 will help you to translate that code to your needs.
The LineCollide%() function above was translated from Unity source code I found on the Internet. The location of that code and any person(s) that need to be given credit are commented within the source code as well. When you use someone else's work you must ALWAYS give them credit where credit is due. Never use copyrighted code unless you have express permission to do so.
The LineCollide%() function above expects two line segments as input and another variable that will be modified when the function ends to contain the x,y coordinate of where the collision took place.
Hit% = LineCollide%(Line2, Line1, Intersect) ' true if the two segments intersect
Hit% will contain a value of -1 (true) if the line segments intersect or a value of 0 (false) if they do not. If there was a collision Intersect will contain the x,y coordinate where the collision occurred.
Using TYPE Defintions To Move Values
For clarity TYPE definitions have been used to hold values relating to x,y coordinates and line segments.
This TYPE definition sets up an x,y coordinate pair.
TYPE A_2D_POINT ' X,Y point definition
x AS INTEGER ' X coordinate location
y AS INTEGER ' Y coordinate location
END TYPE
This TYPE definition then uses the A_2D_POINT definition twice to set up a start x,y coordinate and end x,y coordinate that defines the line segment.
TYPE A_2D_LINE ' line segment definition (notice a TYPE def can be used in another TYPE)
s AS A_2D_POINT ' start of line
e AS A_2D_POINT ' end of line
END TYPE
TYPE definitions can be used within other TYPE definitions to define complex variables.
DIM Line1 AS A_2D_LINE ' a 2D line segment
Line1 is now a complex variable containing two TYPE definitions. To set the start x,y coordinate of this variable:
Line1.s.x = 10
Line1.s.y = 10
line 1's starting x coordinate is 10 and its starting y coordinate is 10. Likewise, to set the end x,y coordinate of this variable:
Line1.e.x = 50
Line1.e.y = 50
line 1's ending x coordinate is 50 and its ending y coordinate is 50.
Passing complex variables into subroutines and functions is simply done using the AS clause and the TYPE definition previously defined:
FUNCTION LineCollide% (L1 AS A_2D_LINE, L2 AS A_2D_LINE, Intersect AS A_2D_POINT)
When a complex variable such as Line1 is passed into the function the entire contents of the complex variable is transferred:
Hit% = LineCollide%(Line2, Line1, Intersect) ' true if the two segments intersect
The local variable L1 now contains the entire contents of Line1. In other words, this is going on in the background:
L1.s.x = Line1.s.x
L1.s.y = Line1.s.y
L1.e.x = Line1.e.x
L1.e.y = Line1.e.y
Since L1 and Line1 are of the same TYPE definition structure the contents are seamlessly moved between them. This behavior works anywhere within code:
Line2 = Line1
If Line2 is defined with the same TYPE definition structure as Line1 then entire contents of Line1 will be copied to Line2.
All of the previous collision demos use this method to pass information to their functions because it's an efficient way to move many values between the main body code and subroutines or functions.
Collision Routines With Intersect Reporting
The line segment collision routine above includes a method of determining where the collision took place. Included in your .\tutorial\Lesson15\ directory are circular, pixel, and rectangular collision routines that will report back collision points as well if you need this feature.
CircCollide.BAS will return the x,y coordinate of the collision location.
PixelCollide.BAS will return the x,y coordinate of the collision location.
RectCollide.BAS will return the rectangular overlapping area of the collision location as an x,y upper left coordinate and an x,y lower right coordinate of the overlapping area.
You can load each one into your IDE and see the change in action.