-
Notifications
You must be signed in to change notification settings - Fork 16
Analytical Geometry: Collision
This page will explain some concepts in the field of analytical geometry, and also how these concepts was structured in the code.
The focus here will be the collision of objects. If you skipped the movement part, please return there. Some subjects will appear again, but concepts will not be repeated.
Rectangles are polygons with 4 vertices. To represent one, we can just store all these vertices positions, and for this we may use 4 vectors to create 1 rectangle.
But there is a better way to do it, just using 2 vectors.
Storing one vertex and the rectangle's center position is enough to represent one. It can be any vertex, but to maintain a pattern we will always save the top right vertex position.
However, the vertex position can't be in reference to the origin, but to the rectangle's center. See the imagine below:
Note that the rectangle rotation is not considered in this way. We will only use rectangles with horizontal and vertical edges.
The three other vertices can be obtained by changing the signal in each coordinate.
Maybe you are still not convinced that storing 2 vectors instead of 4 can be a better approach, because the first way (4 vectors) looks more simple.
Imagine a object moving and that rectangle it's his box collision. The first approach requires that all 4 vectors be updated each time the object moves. The second approach requires that just 1 be updated, the rectangle's center (remember that the top right vector is in reference to the center, so we don't need to update it).
With the vectors already defined, we can use them to create rectangles. See the code below:
Rectangle = {}
Rectangle.__index = Rectangle
function Rectangle:new(c, v)
local R = {}
setmetatable(R, Rectangle)
R.center = Vector:new(c.x, c.y)
R.size = Vector:new(v.x, v.y)
return R
end
-- create 2 rectangles with the same shape, but different positions
local size = Vector:new(1, 2)
local A = Rectangle:new(Vector:new(1, 1), size)
local B = Rectangle:new(Vector:new(1, -2), size)
Object collision in games is never calculated with the real object shape, this would result in a bad performance, because a user device can't handle so much calculations.
A very well know solution for this is use invisible objects to handle the collision. This object have a much simple shape, like circles, triangles or rectangles.
Here we use rectangles. A collision can happen with a point or another rectangle.
To check a collision of a point with a rectangle we first need 2 vertices of the rectangle, the top right (PA) and bottom left (PB).
For the point to be inside, it needs to be between the vertical edges (PA.x, PB.x) and horizontal edges (PA.y and PB.y). Look the image below:
The code for this:
function Rectangle:pointInside(p)
local PA = Vector:new(self.center.x + self.size.x, self.center.y + self.size.y)
local PB = Vector:new(self.center.x - self.size.x, self.center.y - self.size.y)
if PA.x > p.x and PB.x < p.x then
if PA.y > p.y and PB.y < p.y then
return true
end
end
return false
end
To check a collision of a rectangle with another rectangle we first need 2 vertices of each rectangle, the top right (PA and PC) and bottom left (PB and PD).
For the rectangle have intersection, the rightmost vertical edges (PA.x and PC.x) must be higher than the leftmost vertical edges (PB.x and PD.x). The same for horizontal with the top (PA.y and PC.y) and bottom edges (PB.y and PD.y). Look the image below:
The result in code for this is similar to the point collision:
function Rectangle:intersection(b)
local PA = Vector:new(self.center.x + self.size.x, self.center.y + self.size.y)
local PB = Vector:new(self.center.x - self.size.x, self.center.y - self.size.y)
local PC = Vector:new(b.center.x + b.size.x, b.center.y + b.size.y)
local PD = Vector:new(b.center.x - b.size.x, b.center.y - b.size.y)
if PA.x > PD.x and PB.x < PC.x then
if PA.y > PD.y and PB.y < PC.y then
return true
end
end
return false
end
In some cases, just a rectangle for an entire object is enough for the collision. But the better is the precision, the better is the realism in collision.
To get more precision we can create an area by adding many rectangles to cover the object's shape, but by doing this we will rapidly increase the number of collisions to check. To avoid this problem, we can maintain the initial rectangle that contain all the lesser rectangles that gives precision.
Now, we continue to check the collision with bigger rectangles, and only when a collision is found, we check the lesser rectangles. If none of those return a collision detection, then we discard the situation as a collision.
The class Area will have the bigger rectangle as the area size, and the smaller rectangles in a table:
Area = {}
Area.__index = Area
function Area:new(size)
local A = {}
setmetatable(A, Area)
-- copy the rectangle
A.size = Rectangle:new(Vector:new(0, 0), Vector:new(size.x, size.y))
A.hitbox = {}
return A
end
-- add a rectangle to the area, inserting it in hitbox
function Area:newRectangularArea(c, v)
local rectangle = Rectangle:new(c, v)
table.insert(self.hitbox, rectangle)
end
The rectangles in hitbox table not just have a size in reference to their center, but also his center have reference to the Area center. This might look complex (and it is), but there is example below in this page that will better show this.
To detect a collision between areas we must first check if the bigger rectangle (size) have collision, if not, them none of the hit boxes would have collision, if size detect a collision, then we must check collision of all hit boxes.
The more hit boxes the objects have, more precise will be the detection, but more calculations will be required.
function Area:detectCollision(orientationA, posA, b, orientationB, posB)
-- self = area A
-- b = area B
-- orientation = if it is inverted
-- copy return a rectangle copy with the center in posA and orientationA
-- the center reference will be the origin
local sizeA = self.size:copy(orientationA, posA)
local sizeB = b.size:copy(orientationB, posB)
local intersection = false
if sizeA:intersection(sizeB) then
local i = 1
while not intersection and i <= #self.hitbox do
local rectangleA = self.hitbox[i]:copy(orientationA, posA)
local j = 1
while not intersection and j <= #b.hitbox do
local rectangleB = b.hitbox[j]:copy(orientationB, posB)
intersection = rectangleA:intersection(rectangleB)
j = j + 1
end
i = i + 1
end
end
return intersection
end
Here is an example checking collision between 2 objects, which results false (note that if it was just the bigger rectangles, it would be a collision):
local posA = Vector:new(3, 3)
local posB = Vector:new(7, 5)
local orientation = Vector:new(0, 1)
-- create the area
local areaA = Area:new(Vector:new(3, 3))
local areaB = Area:new(Vector:new(2, 2))
-- create the smaller rectangles for areaA
areaA:newRectangularArea(
Vector:new(0, 0),
Vector:new(3, 1))
areaA:newRectangularArea(
Vector:new(0, 0),
Vector:new(1, 3))
-- create the smaller rectangles for areaB
areaB:newRectangularArea(
Vector:new(-1, 0),
Vector:new(1, 1))
areaB:newRectangularArea(
Vector:new(1, 0),
Vector:new(1, 2))
local collision = areaA:detectCollision(orientation, posA, areaB, orientation, posB)
print(collision) -- false