Hi,
Pixel-perfect collision detection demo; it was fun to create. There's definitely room for performance improvements.
Pixel-perfect collision detection demo; it was fun to create. There's definitely room for performance improvements.
Code: (Select All)
' This program demonstrates the application of a collision detection algorithm in 3 steps:
'
' Step 1. Inspecting areas to identify where at least 2 shapes are in.
' Step 2. For each area, checking the distance between the present shapes to determine
' if they could potentially be in contact.
' Step 3. Testing for overlap in each area where the shapes are potentially in contact
' using semi-transparent shape masks.
type point_type
x as double
y as double
end type
type shape_type
index as integer ' shape id
radius as integer ' half size of the shape
firstPointIndex as integer ' first pointer to the points array for vertices
lastPointIndex as integer ' lasr pointer for vertices
x as double ' shape position
y as double
rgbaColor as long ' base color
speed as double ' movement speed
direction as double ' direction speed
rotation as double ' angular speed (radians)
orientation as double ' angular orientation (radians)
drawString as string ' draw description
area as integer ' area where the center of the shape is located
areaX as integer '
areaY as integer '
imageHandle as long ' stock shape drawing
maskHandle as long ' stock mask drawing
maskComputed as integer ' mask indicator (-1 = computed / 0 = not computed)
collid as integer ' collision indicator (-1 = collision detected)
end type
const SHAPES_COUNT = 128 ' number of shapes
const MIN_SHAPE_RADIUS = 5 ' minimum radius of the circle containing a shape
const MAX_SHAPE_RADIUS = 30 ' maximum radius of the circle containing a shape
const MIN_SHAPE_VERTICES = 5 ' minimum number of vertices of a shape
const MAX_SHAPE_VERTICES = 15 ' maximum number of vertices of a shape
const MIN_SHAPE_SPEED = 0.5 ' minimum speed of a shape
const MAX_SHAPE_SPEED = 1.1 ' maximum speed of a shape
const PIXELS_PRECISION = 2 ' collision pixels precision
type area_type
shapeLocator as string * SHAPES_COUNT ' Character array to memorize shapes in area
column as integer ' area colum
row as integer ' area row
x as integer ' top-left position of the area on the screen
y as integer
center as point_type ' center position of the area onthe screen
end type
const DRAW_MODE = 1 ' 0 = use DRAW instruction / 1 = use LINE instruction
const STEP_MODE_KEY = "S" ' toggle stepmode
step_mode_activated% = 0 ' -1 = step by step / 0 = don't
const SHOW_DETECTION_KEY = "D" ' toggle drawing proximity detection areas
show_detection_activated% = 0 ' -1 = show proximity detections areas / 0 = don't
const SHOW_GRID_KEY = "G" ' toggle drawing grid to show area delimitations
show_grid_activated% = 0 ' -1 = show grid of areas / 0 = dont't
const SHOW_COLLISIONS_KEY = "C" ' toogle drawing areas with potential collisions
show_collisions_activated% = 0 ' -1 = show areas where collisions are potentials / 0 = dont't
const EXPECTED_FPS = 60 ' Expected Frames Per Second
const VIEWPORT_WIDTH = 440 ' viewport width
const VIEWPORT_HEIGHT = 440 ' viewport height
dim shared TAU as single ' 2 * PI
TAU = 8 * atn(1)
redim shapes(-1) as shape_type ' array containing the shapes description
redim points(0) as point_type ' array containing coordinates of the vertices of the shapes
' definition of the page dimension in which the shapes are drawn
const X_DECAY = _ceil(2.5 * MAX_SHAPE_RADIUS) ' offset between canvas and viewport
const Y_DECAY = _ceil(2.5 * MAX_SHAPE_RADIUS)
const CANVAS_WIDTH = VIEWPORT_WIDTH + 2 * X_DECAY ' canvas size
const CANVAS_HEIGHT = VIEWPORT_HEIGHT + 2 * Y_DECAY
' areas definitions
const AREAS_WIDTH = _ceil(CANVAS_WIDTH / MAX_SHAPE_RADIUS) ' number of areas in a row
const AREAS_HEIGHT = _ceil(CANVAS_HEIGHT / MAX_SHAPE_RADIUS) ' number of areas in a colum
const AREAS_COUNT = AREAS_WIDTH * AREAS_HEIGHT ' total number of areas (size of areas array)
const WIDTH_OF_AREA = 2 * MAX_SHAPE_RADIUS ' size of an area
const HEIGHT_OF_AREA = 2 * MAX_SHAPE_RADIUS
' areas array initialisation
redim areas(0 to AREAS_COUNT) as area_type
for i%=lbound(areas) to ubound(areas)
areas(i%).column = i% mod AREAS_WIDTH
areas(i%).row = i% \ AREAS_WIDTH + 1
areas(i%).x = areas(i%).column * MAX_SHAPE_RADIUS
areas(i%).y = areas(i%).row * MAX_SHAPE_RADIUS
areas(i%).center.x = areas(i%).x + MAX_SHAPE_RADIUS
areas(i%).center.y = areas(i%).y + MAX_SHAPE_RADIUS
next i%
' set random draw
randomize timer
' home screen
screen 0
cls
color _rgb(63,160,0)
locate 4,1
? " ========================"
? " Collision Detection Demo"
? " ========================"
?
? " What this program does:"
? " - Generates moving and rotating irregular polygonal shapes."
? " - Uses spatial partitioning (areas) to find near neighbours."
? " - Performs circle-circle and pixel-level mask collision tests."
? " - Show collisions by changing the color of the affected shapes."
?
? " Keyboard shortcuts:"
? " S - Toggle step mode (when active, press any key to advance a frame)"
? " D - Toggle drawing of proximity detection areas"
? " G - Toggle grid of areas"
? " C - Toggle highlighting of areas with potential collisions"
? " Esc - Quit"
?
? " Press any key to start..."
do : k$ = inkey$ : loop while k$ = ""
' generate all shapes
for i% = 1 to SHAPES_COUNT
generateShape _
shapes(), points(), _
MIN_SHAPE_RADIUS, MAX_SHAPE_RADIUS, _
MIN_SHAPE_VERTICES, MAX_SHAPE_VERTICES, _
MIN_SHAPE_SPEED, MAX_SHAPE_SPEED
next i%
' prepare screen rendering
viewport& = _newimage(VIEWPORT_WIDTH,VIEWPORT_HEIGHT,32)
canvas& = _newimage(CANVAS_WIDTH,CANVAS_HEIGHT,32)
textview& = _newimage(CANVAS_WIDTH,64,32)
screen viewport&
' timer to compute frames per second
t = timer(.001)
' main loop
do
_limit EXPECTED_FPS
_dest canvas&
cls
' draw shapes
for i%=lbound(shapes) to ubound(shapes)
drawShape shapes(i%), points()
shapes(i%).collid = 0
next i%
' draw grid
if show_grid_activated% then
for x% = 0 to AREAS_WIDTH
xx%= (x% + 0.5) * MAX_SHAPE_RADIUS
line (xx%,0)-(xx%,CANVAS_HEIGHT),_rgba(255,255,255,63)
next x%
for y% = 0 to AREAS_HEIGHT
yy% = (y% + 0.5) * MAX_SHAPE_RADIUS
line (0,yy%)-(CANVAS_WIDTH,yy%),_rgba(255,255,255,63)
next y%
end if
' init areas for collision detection
for i% = lbound(areas) to ubound(areas)
areas(i%).shapeLocator = space$(SHAPES_COUNT)
next i%
' move shapes
for i%=lbound(shapes) to ubound(shapes)
moveShape shapes(i%), points()
next i%
' compute proximity detections areas
' and draw on demand
for i%=lbound(shapes) to ubound(shapes)
redim targets(-1) as integer
evaluateShapeSector shapes(i%), areas(), targets()
if show_detection_activated% then
if shapes(i%).area>=0 then
for j% = 0 to ubound(targets)
x% = targets(j%) mod AREAS_WIDTH
y% = targets(j%) \ AREAS_WIDTH + 1
circle ( _
x% * MAX_SHAPE_RADIUS, _
y% * MAX_SHAPE_RADIUS _
), MAX_SHAPE_RADIUS\2, _rgba(255,255,255,127)
next j%
end if
end if
next i%
' detect collisions
' and draw ondemand
for i% = lbound(areas) to ubound(areas)
if instr(areas(i%).shapeLocator,"*") <> _instrrev(areas(i%).shapeLocator,"*") then
collid% = collision(shapes(),areas(i%))
' draw potentials collisions areas
if show_collisions_activated% then
if collid% then
c~& = _rgb(255,0,0)
else
c~& = _rgba(255,255,0,127)
end if
circle ( _
areas(i%).x, _
areas(i%).y _
), MAX_SHAPE_RADIUS\2, c~&
end if
end if
next i%
color &HFFFFFFFF
_printstring (100,100), "fps:"+str$(fps%)
frames% = frames% + 1
if timer(.001)>=t+1 then
fps% = frames%
frames% = 0
t = timer(.001)
end if
' display result
_putimage (- X_DECAY,- Y_DECAY), canvas&, viewport&
' keyboard interaction
do
key$ = inkey$
if key$ = chr$(27) then
system
end if
select case ucase$(key$)
case STEP_MODE_KEY
step_mode_activated% = - step_mode_activated% - 1
case SHOW_DETECTION_KEY
show_detection_activated% = - show_detection_activated% - 1
case SHOW_GRID_KEY
show_grid_activated% = - show_grid_activated% - 1
case SHOW_COLLISIONS_KEY
show_collisions_activated% = - show_collisions_activated% - 1
end select
if step_mode_activated% then
if key$ <> "" then exit do
else
exit do
end if
loop
loop
' generate a polygonal shape
' with movement attributes
sub generateShape ( _
shapes() as shape_type, _
points() as point_type, _
minRadius as integer, maxRadius as integer, _
minPoints as integer, maxPoints as integer,_
minSpeed as double, maxSpeed as double _
)
dim shape as shape_type
dim pointsCounter as integer
shape.radius = int(rnd * (maxRadius - minRadius)) + minRadius
pointsCounter = int(rnd * (maxPoints - minPoints)) + minPoints
shape.firstPointIndex = ubound(points) + 1
shape.lastPointIndex = ubound(points) + pointsCounter
shape.rgbaColor = _rgb32(int(rnd*127)+128,int(rnd*127)+128,int(rnd*127)+128)
shape.x = int(rnd*CANVAS_WIDTH)
shape.y = int(rnd*CANVAS_HEIGHT)
shape.speed = rnd*(maxSpeed - minSpeed) + minSpeed
shape.direction = rnd*TAU
shape.rotation = (rnd*TAU - TAU/2)/100
shape.orientation = 0
shape.imageHandle = _newimage(2*MAX_SHAPE_RADIUS,2*MAX_SHAPE_RADIUS,32)
shape.maskHandle = _newimage(2*MAX_SHAPE_RADIUS\PIXELS_PRECISION,2*MAX_SHAPE_RADIUS\PIXELS_PRECISION,32)
redim _preserve points(1 to shape.lastPointIndex) as point_type
delta! = TAU / pointsCounter
angle! = 0
for i% = shape.firstPointIndex to shape.lastPointIndex
radius% = int(rnd * (shape.radius - minRadius)) + minRadius
points(i%).x = int(cos(angle!) * radius%)
points(i%).y = int(sin(angle!) * radius%)
angle! = angle! + delta!
next i%
convertShapeToDrawString shape, points()
redim _preserve shapes(1 to ubound(shapes) + 1) as shape_type
shape.index = ubound(shapes)
shapes(ubound(shapes)) = shape
end sub
' compute a shape movement and rotation
sub moveShape ( shape as shape_type, points() as point_type )
shape.x = shape.x + cos(shape.direction) * shape.speed
shape.y = shape.y + sin(shape.direction) * shape.speed
if shape.x<0 then
shape.x = CANVAS_WIDTH - shape.x
elseif shape.x>CANVAS_WIDTH then
shape.x = - (shape.x - CANVAS_WIDTH)
end if
if shape.y<0 then
shape.y = CANVAS_HEIGHT - shape.y
elseif shape.y > CANVAS_HEIGHT then
shape.y = - (shape.y - CANVAS_HEIGHT)
end if
shape.orientation = shape.orientation + shape.rotation
if shape.orientation < 0 then
shape.orientation = TAU + shape.orientation
elseif shape.orientation > TAU then
shape.orientation = shape.orientation - TAU
end if
for i%=shape.firstPointIndex to shape.lastPointIndex
x! = points(i%).x
y! = points(i%).y
rotation x!, y!, shape.rotation
points(i%).x = x!
points(i%).y = y!
next i%
convertShapeToDrawString shape, points()
' shape change the mask is not valid
shape.maskComputed = 0
end sub
' helper to angular rotation
sub rotation (x!, y!, a!)
xr! = x! * cos(a!) - y! * sin(a!)
yr! = x! * sin(a!) + y! * cos(a!)
x! = xr!
y! = yr!
end sub
' convert a points array to DRAW string sequence
sub convertShapeToDrawString (shape as shape_type, points() as point_type)
shape.drawString = ""
n% = 0
for i% = shape.firstPointIndex to shape.lastPointIndex
x% = int(points(i%).x + shape.x)
y% = int(points(i%).y + shape.y)
if x%<0 or y%<0 then n% = -1
rx$ = _tostr$(x%)
ry$ = _tostr$(y%)
shape.drawString = shape.drawString + "M " + rx$ + ", " + ry$
next i%
x% = int(points(shape.firstPointIndex).x + shape.x)
y% = int(points(shape.firstPointIndex).y + shape.y)
rx$ = _tostr$(x%)
ry$ = _tostr$(y%)
shape.drawString = shape.drawString + "M " + rx$ + ", " + ry$
if n% then shape.drawString = ""
end sub
' draw shape
' CAUTION : the DRAW mode is not efficient, prefer then LINE mode
sub drawShape ( shape as shape_type, points() as point_type )
h& = _dest
_dest shape.imageHandle
cls , &H00FFFFFF
dim rgbaColor as _unsigned long
rgbaColor = shape.rgbaColor
if shape.collid then rgbaColor = _rgba(255,255,0,255)
select case DRAW_MODE
case 0
if shape.drawString<>"" then
draw "C"+str$(shape.rgbaColor)
draw "B"+shape.drawString
paint (MAX_SHAPE_RADIUS,MAX_SHAPE_RADIUS),rgbaColor and &H7FFFFFFF ,rgbaColor
end if
case 1
pset (points(shape.firstPointIndex).x + MAX_SHAPE_RADIUS, points(shape.firstPointIndex).y + MAX_SHAPE_RADIUS), rgbaColor
for i%=shape.firstPointIndex to shape.lastPointIndex
line - (points(i%).x + MAX_SHAPE_RADIUS, points(i%).y + MAX_SHAPE_RADIUS), rgbaColor
next i%
line - (points(shape.firstPointIndex).x + MAX_SHAPE_RADIUS, points(shape.firstPointIndex).y + MAX_SHAPE_RADIUS), rgbaColor
paint (MAX_SHAPE_RADIUS,MAX_SHAPE_RADIUS),rgbaColor and &H7FFFFFFF ,rgbaColor
end select
_putimage (shape.x - MAX_SHAPE_RADIUS, shape.y - MAX_SHAPE_RADIUS), shape.imageHandle, h&
_dest h&
end sub
' generates a semi-transparent mask image
sub computeMask ( shape as shape_type )
h& = _dest
_source shape.imageHandle
_dest shape.maskHandle
cls , &H00000000
m% = 2 * MAX_SHAPE_RADIUS - 1
for x% = 0 to m% step PIXELS_PRECISION
for y% = 0 to m% step PIXELS_PRECISION
if point(x%,y%) <> &H00FFFFFF then
pset(x%\PIXELS_PRECISION,y%\PIXELS_PRECISION), &H7FFFFFFF
end if
next y%
next x%
' the mask is now valid
shape.maskComputed = -1
_dest h&
end sub
' calculation of the areas affected by the shape
sub evaluateShapeSector ( shape as shape_type, areas() as area_type, targets() as integer )
x% = shape.x
y% = shape.y
' shape outside the canvas, so not in an area
if x% < 0 or x% > CANVAS_WIDTH or y% < 0 or y% > CANVAS_HEIGHT then
shape.area = -1
exit sub
end if
' computes area
shape.areaX = _round(x% / MAX_SHAPE_RADIUS)
shape.areaY = _round(y% / MAX_SHAPE_RADIUS)
shape.area = (shape.areaY - 1) * AREAS_WIDTH + shape.areaX
' search other potential areas around
if shape.area>=0 then
redim targets(-1) as integer
x0! = shape.x - shape.radius
y0! = shape.y - shape.radius
sx0% = _round(x0! / MAX_SHAPE_RADIUS)
sy0% = _round(y0! / MAX_SHAPE_RADIUS)
x2! = shape.x + shape.radius
y2! = shape.y + shape.radius
sx2% = _round(x2! / MAX_SHAPE_RADIUS)
sy2% = _round(y2! / MAX_SHAPE_RADIUS)
s0% = (sy0% - 1) * AREAS_WIDTH + sx0%
s1% = (sy0% - 1) * AREAS_WIDTH + shape.areaX
s2% = (sy0% - 1) * AREAS_WIDTH + sx2%
s3% = (shape.areaY - 1) * AREAS_WIDTH + sx0%
s4% = (shape.areaY - 1) * AREAS_WIDTH + shape.areaX
s5% = (shape.areaY - 1) * AREAS_WIDTH + sx2%
s6% = (sy2% - 1) * AREAS_WIDTH + sx0%
s7% = (sy2% - 1) * AREAS_WIDTH + shape.areaX
s8% = (sy2% - 1) * AREAS_WIDTH + sx2%
addUniqueInteger targets(), s0%
addUniqueInteger targets(), s1%
addUniqueInteger targets(), s2%
addUniqueInteger targets(), s3%
addUniqueInteger targets(), s4%
addUniqueInteger targets(), s5%
addUniqueInteger targets(), s6%
addUniqueInteger targets(), s7%
addUniqueInteger targets(), s8%
' remembers the presence of the shape in the area in the character array
for i% = 0 to ubound(targets)
if targets(i%)>=0 and targets(i%)<=AREAS_COUNT then mid$(areas(targets(i%)).shapeLocator,shape.index,1) = "*"
next i%
end if
end sub
' helper to keep only unique values
sub addUniqueInteger ( t() as integer, v as integer )
for i%=lbound(t) to ubound(t)
if t(i%)=v then exit sub
next i%
redim _preserve t(ubound(t)+1) as integer
t(ubound(t)) = v
end sub
' detects collisions between shapes
function collision% ( shapes() as shape_type, area as area_type )
res% = 0
redim obj(-1) as integer
' collect unique shape indices from the area string (positions of '*')
p0% = instr(area.shapeLocator,"*")
do until p0% = 0
s0% = p0%
found% = 0
for i%=lbound(obj) to ubound(obj)
if obj(i%) = s0% then
found% = -1
exit for
end if
next i%
if not found% then
redim _preserve obj(ubound(obj)+1) as integer
obj(ubound(obj)) = s0%
end if
p0% = instr(p0%+1,area.shapeLocator,"*")
loop
' pairwise collision test (circle-circle) then test pixel-perfect
' when the distance is short enough to have a possible collision
for i%=lbound(obj) to ubound(obj)
for j%=i%+1 to ubound(obj)
if shapes(obj(i%)).collid and shapes(obj(j%)).collid then exit for
h% = _hypot(shapes(obj(i%)).x-shapes(obj(j%)).x,shapes(obj(i%)).y-shapes(obj(j%)).y)
r% = shapes(obj(i%)).radius + shapes(obj(j%)).radius
if h% <= r% then
res% = fusion%(shapes(obj(i%)),shapes(obj(j%)),area)
if res% then
shapes(obj(i%)).collid = -1
shapes(obj(j%)).collid = -1
end if
end if
next j%
next i%
collision% = res%
end function
' detects the overlapping of shapes
'
' each shape mask is semi-transparent ;
' copies each shape mask to its actual position in the area ;
' if we find at least one point that is no longer transparent
' then there is overlap and therefore collision detection
function fusion% ( shapeA as shape_type, shapeB as shape_type, area as area_type )
if not shapeA.maskComputed then
computeMask shapeA
end if
if not shapeB.maskComputed then
computeMask shapeB
end if
res% = 0
m% = 2 * MAX_SHAPE_RADIUS \ PIXELS_PRECISION - 1
buffer& = _newimage(WIDTH_OF_AREA\PIXELS_PRECISION,HEIGHT_OF_AREA\PIXELS_PRECISION,32)
_putimage ( (shapeA.x - area.center.x)\PIXELS_PRECISION, (shapeA.y - area.center.y)\PIXELS_PRECISION ), shapeA.maskHandle, buffer&
_putimage ( (shapeB.x - area.center.x)\PIXELS_PRECISION, (shapeB.y - area.center.y)\PIXELS_PRECISION ), shapeB.maskHandle, buffer&
_source buffer&
for x% = 0 to m%
for y% = 0 to m%
if _alpha32(point(x%,y%)) > &H7F then
res% = -1
exit for
end if
next y%
next x%
_freeimage buffer&
fusion% = res%
end function



![[Image: Accurate-Collision.png]](https://i.ibb.co/ndpGL83/Accurate-Collision.png)