I am working on creating Pong on an FPGA using VHDL. I have been racking my brain for days trying to figure out how to do and what is the best solution to rectangle rectangle collision and I think I have come up with the best solution although there seems to be one bug (explained below).
I took the advice from larsbutler's answer and have used this strategy to go about collision:
- object.positionX += object.velocityX
- check/respond collisions
- object.positionY += object.velocityY
- check/respond collisions
This pseudo-code explains how I check/respond to collisions:
// right edge of ball in between objects left and right edge
// OR
// left edge of ball in between objects left and right edge
if((ball.right >= object.left && ball.right <= ball.right) || (ball.left >= object.left && ball.left <= object.right))
{
xCollision = true;
}
// top edge of ball in between objects top and bottom edge
// OR
// bottom edge of ball in between objects top and bottom edge
if((ball.top >= object.top && ball.top <= object.bottom) || (ball.bottom <= object.bottom && ball.bottom >= object.top))
{
yCollision = true;
}
// respond to collision
if xCollision and yCollision then
{
// This code block is respective to each x or y update in order to resolve collision
}
Keep in mind that the top left corner of the screen is (0, 0). Objects are positioned from their center. Here is a diagram:
This is basic diagram of what I want the response to be: (source)
At this moment I am just trying to work on the x collision. The problem enlies in the xPosition code to get the ball out of the paddle to avoid getting stuck. It seems that if xVelocity < 0 then
does not evaluate right. Say the ball is traveling left to right(xVelocity > 0) and then we hit the paddle on the right side. The xVelocity will change signs to negative(xVelocity < 0). The problem if statement should evaluate to true and decrement the xPosition to get it out of the paddle. This does not happen though and instead jumps across the paddle and just repeats back and forth. The reason we add or subtract 40 is for testing and will actually be the amount it is inside the paddle.
Many of my implementations seem to fall into this pitfall xVelocity not evaluating right. The code works if you switch the plus and minus in the if else but that does not make any logical sense in my mind. Why must it be the opposite of what I have below? (keep in mind that the xVelocity is multiplied by -1 before this.
-- respond to collision
if xCollision = '1' and yCollision = '1' then
-- Change direction
xVelocity <= xVelocity * (-1);
-- Add glancing y velocity of paddle
yVelocity <= yVelocity + (collisionObjects(i)(5)/4);
-- If bouncing in the left direction
if xVelocity < 0 then
-- move outwards as much as we are inside the paddle
-- Should be negating from xPosition as we are bouncing left and want to resolve that way
xPosition <= xPosition - 40;
else
xPosition <= xPosition + 40;
end if;
end if;
-- Ball collision is using discrete collision!
-- not sweep collision which helps with small fast objects passing through each other
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.std_logic_unsigned.all;
--note this line.The package is compiled to this directory by default.
--so don't forget to include this directory.
library work;
--this line also is must.This includes the particular package into your program.
use work.my_package.all;
entity Ball is
generic(
numCollisionObjects: integer := 2;
ballRadius : integer := 10;
rgbColor : std_logic_vector(7 downto 0) := "111" & "111" & "11"
);
port(
reset: in std_logic;
clk: in std_logic;
hCounter: in std_logic_vector(9 downto 0);
vCounter: in std_logic_vector(9 downto 0);
colObject: out type_collisionObject;
collisionObjects: in type_collisionObjectArray(0 to numCollisionObjects-1);
pixelOn: out std_logic;
rgbPixel: out std_logic_vector(7 downto 0) := rgbColor
);
end Ball;
architecture Behavioral of Ball is
signal xPosition : integer := 0;
signal yPosition : integer := 0;
signal xVelocity : integer := 0;
signal yVelocity : integer := 0;
signal pixelBuffer : std_logic;
signal RGBBuffer : std_logic_vector(7 downto 0) := rgbColor;
signal colObjectBuffer: type_collisionObject;
begin
pixelOn <= pixelBuffer;
rgbPixel <= RGBBuffer;
colObjectBuffer <= (xPosition, yPosition, ballRadius * 2, ballRadius * 2, xVelocity, yVelocity);
colObject <= colObjectBuffer;
animation: process(clk)
variable update_clk_count: natural := 0;
variable update_clk_prescaler: natural := 10000000; -- 833333; -- Slow because of debuging... 50 Mhz / clk_prescaler = desired speed
--variable i: natural := 1;
variable xCollision: std_logic := '0';
variable yCollision: std_logic := '0';
variable colObject_lastState: type_collisionObject;
begin
if rising_edge(clk) then
-- While reset is high then we reset the positions
if reset = '1' then
xPosition <= SCREEN_RESX/2;
yPosition <= SCREEN_RESY/2;
xVelocity <= 3;
yVelocity <= 0;
else
if update_clk_count >= update_clk_prescaler then
colObject_lastState := colObjectBuffer;
-- if we are hitting the left wall
if (xPosition - ballRadius + xVelocity) <= 0 then
RGBBuffer <= rgbColor;
if xVelocity < 0 then
xVelocity <= xVelocity * (-1);
end if;
end if;
-- if we are hitting the right wall
if (xPosition + ballRadius + xVelocity) >= 640 then
RGBBuffer <= rgbColor;
if xVelocity > 0 then
xVelocity <= xVelocity * (-1);
end if;
end if;
-- if we are hitting the top wall
if (yPosition - ballRadius + yVelocity) <= 0 then
RGBBuffer <= rgbColor;
if yVelocity < 0 then
yVelocity <= yVelocity * (-1);
end if;
end if;
-- if we are hitting the bottom wall
if (yPosition + ballRadius + yVelocity) >= 480 then
RGBBuffer <= rgbColor;
if yVelocity > 0 then
yVelocity <= yVelocity * (-1);
end if;
end if;
-- Update x position
xPosition <= xPosition + xVelocity;
-- Check for collision after x updates
if not(xVelocity = 0) then
for i in collisionObjects'range loop
xCollision := '0';
yCollision := '0';
-- right edge of ball in between objects left and right edge
-- OR
-- left edge of ball in between objects left and right edge
if (xPosition + ballRadius >= collisionObjects(i)(0) - (collisionObjects(i)(2)/2) and xPosition + ballRadius <= collisionObjects(i)(0) + (collisionObjects(i)(2)/2))
OR (xPosition - ballRadius >= collisionObjects(i)(0) - (collisionObjects(i)(2)/2) and xPosition - ballRadius <= collisionObjects(i)(0) + (collisionObjects(i)(2)/2)) then
xCollision := '1';
end if;
-- top edge of ball in between objects top and bottom edge
-- OR
-- bottom edge of ball in between objects top and bottom edge
if (yPosition - ballRadius >= collisionObjects(i)(1) - (collisionObjects(i)(3)/2) and yPosition - ballRadius <= collisionObjects(i)(1) + (collisionObjects(i)(3)/2))
OR (yPosition + ballRadius >= collisionObjects(i)(1) - (collisionObjects(i)(3)/2) and yPosition + ballRadius <= collisionObjects(i)(1) + (collisionObjects(i)(3)/2)) then
yCollision := '1';
end if;
-- respond to collision
if xCollision = '1' and yCollision = '1' then
-- Change direction
xVelocity <= xVelocity * (-1);
-- Add glancing y velocity of paddle
yVelocity <= yVelocity + (collisionObjects(i)(5)/4);
-- If bouncing in the left direction
if xVelocity < 0 then
-- move outwards as much as we are inside the paddle
-- Should be negating from xPosition as we are bouncing left and want to resolve that way
xPosition <= xPosition - 40;
else
xPosition <= xPosition + 40;
end if;
end if;
end loop;
end if;
-- Update y position
yPosition <= yPosition + yVelocity;
-- Check for collision after y updates
if not(yVelocity = 0) then
for i in collisionObjects'range loop
xCollision := '0';
yCollision := '0';
-- right edge of ball in between objects left and right edge
-- OR
-- left edge of ball in between objects left and right edge
if (xPosition + ballRadius >= collisionObjects(i)(0) - (collisionObjects(i)(2)/2) and xPosition + ballRadius <= collisionObjects(i)(0) + (collisionObjects(i)(2)/2))
OR (xPosition - ballRadius >= collisionObjects(i)(0) - (collisionObjects(i)(2)/2) and xPosition - ballRadius <= collisionObjects(i)(0) + (collisionObjects(i)(2)/2)) then
xCollision := '1';
end if;
-- top edge of ball in between objects top and bottom edge
-- OR
-- bottom edge of ball in between objects top and bottom edge
if (yPosition - ballRadius >= collisionObjects(i)(1) - (collisionObjects(i)(3)/2) and yPosition - ballRadius <= collisionObjects(i)(1) + (collisionObjects(i)(3)/2))
OR (yPosition + ballRadius >= collisionObjects(i)(1) - (collisionObjects(i)(3)/2) and yPosition + ballRadius <= collisionObjects(i)(1) + (collisionObjects(i)(3)/2)) then
yCollision := '1';
end if;
-- respond to collision
if xCollision = '1' and yCollision = '1' then
yVelocity <= yVelocity * (-1);
-- If ball is moving in same direction the paddle is
if (yVelocity < 0 and collisionObjects(i)(5) < 0)
OR (yVelocity > 0 and collisionObjects(i)(5) > 0) then
yVelocity <= yVelocity + (collisionObjects(i)(5)/2);
end if;
end if;
end loop;
end if;
update_clk_count := 0;
end if;
end if;
update_clk_count := update_clk_count + 1;
end if;
end process;
drawing: process(hCounter, vCounter)
begin
-- If within pixel bounds of bar
if hCounter >= (xPosition - ballRadius) and hCounter <= (xPosition + ballRadius) and vCounter >= (yPosition - ballRadius) and vCounter <= (yPosition + ballRadius) then
pixelBuffer <= '1';
else
pixelBuffer <= '0';
end if;
end process;
end Behavioral;
And the relevant information from my_package.vhd:
constant SCREEN_RESX: integer := 640;
constant SCREEN_RESY: integer := 480;
-- 0: position X
-- 1: position Y
-- 2: size X
-- 3: size Y
-- 4: velocityX
-- 5: velocityY
type type_collisionObject is array (0 to 5) of integer;
type type_collisionObjectArray is array(natural range <>) of type_collisionObject;
My collision detection is not bullet proof nor working to my satisfaction but I did seem to find my bug. I had no clue but in VHDL a signal does not update its value until the end of a process and will update to the last statement. Meaning if you turn it negative and then add onto it, you will only get the addition.
I wish this was stressed more in guides and tutorials because this cost me loads of time.
I have not inspected every line of your code in detail but it looks as if you have introduced many opportunities for bugs in your translation from pseudocode to VHDL. Even if the transcoding is completely correct, it is much harder to trace to/from the pseudocode than it needs to be...
Assuming you trust the pseudocode (and I can see some problems with it :-)
Why not translate
if((ball.right >= object.left && ball.right <= ball.right) || (ball.left >= object.left && ball.left <= object.right))
{
xCollision = true;
}
into VHDL (correcting the obvious) as
if (ball.right >= object.left and ball.right <= object.right)
or (ball.left >= object.left and ball.left <= object.right) then
xCollision := true;
end if;
or simply
xCollision := (ball.right >= object.left and ball.right <= object.right)
or (ball.left >= object.left and ball.left <= object.right);
and create the datatypes for ball
, object
(records) and xCollision
(boolean) appropriately?
Go the next step and wrap that as a function or procedure:
function XCollision (ball, object : rect) return boolean is
begin
return (ball.right >= object.left and ball.right <= ball.right)
or (ball.left >= object.left and ball.left <= object.right);
end XCollision;
callable as
x_collision := false;
for i in objects'range loop
x_collision := x_collision or XCollision (ball, objects(i));
end loop;
VHDL is a high level language offering a powerful type model - probably better than the language your pseudocode is written in. Use it that way. (You have already made a good start using packages).
You will have a much easier time of writing, analysing and debugging your VHDL. For an alternative to using records, create an enumeration and use it to index an array of values.
type Sides is (left, right, top, bottom);
type obj_type is array (sides) of integer;
variable ball, object : obj_type;
in which case XCollision can now be
function XCollision (ball, object : rect) return boolean is
begin
return (ball(right) >= object(left) and ...);
end XCollision;
There is nothing fundamentally non-synthesisable in the above.
Though I have seen a 2010 build of Synplicity object to certain functions - it lags considerably behind XST in some respects - strangely enough it was happy with the equivalent procedure with an OUT parameter for the return value.
Those who say that programming for FPGA (designing hardware) is different from software coding are absolutely correct - within limits - but approximately NEVER when they recommend crippling your ability to create clean, understandable code that says what it does and does what it says.
Where hardware design DOES differ is in things like: