Collision Detection

From Arduboy Wiki
Revision as of 04:45, 16 August 2024 by Filmote (talk | contribs)
Jump to navigation Jump to search

The Arduboy2 library provides functions to detect when two rectangles or a rectangle and a point overlap. It is implemented in such a way that is independent of other constructs such as the screen and sprites. In practice though, the detection of collisions is often focused on the collision of sprites (players, bullets, etc) with other entities (enemy sprites, background elements or other play elements). These collisions may occur onscreen or off and functions within the library are flexible enough to cater for this.

The following examples and code assume you are familiar with the concept and functions available to render sprites. More information on this can be found in the article covering the Sprites / SpritesB Library.

But how does the collision detection work?

The Arduboy2 library provides two collision detection methods which are shown below.

bool collide(Point point, Rect rect);
bool collide(Rect rect1, Rect rect2);

It also provides two structures to assist.

struct Rect {
  int16_t x;
  int16_t y;
  uint8_t width;
  uint8_t height; 
};

struct Point {
  int16_t x; 
  int16_t y; 
};

The structures describe a rectangle and a point in space - the x / y coordinates are arbitrary and their origin can be anywhere but it typical to align these with screen's coordinates. Thus 0, 0 is located at the top-left corner of the screen with negative x and y values representing locations to the left and above the screen respectively.

The first function tests to see if a point is contained within the specified rectangle. The sample code below demonstrates this:

Point p;
Rect r;

p.x = 5;
p.y = 5;

r.x = 2;
r.y = 2;
r.width = 10;
r.height = 10;

Serial.println(arduboy.collision(p, r));    


Running the above code will print 1 as the point at position 5, 5 is in the centre of the square whose top left corner is at position 2, 2 and lower right corner is at 11, 11.

Moving the point outside of the square with the following code:

p.x = 14;
p.y = 2;

Serial.println(arduboy.collision(p, r));    








Will print 0 as the point at position 14, 2 is outside of the square whose top left corner is at position 5, 5 and lower right corner is at 15, 15.

The second function compares the position of two rectangles and tests to see if there is any overlap. This is demonstrated in the code below:

Rect r1 = {2, 2, 5, 5};
Rect r2 = {5, 5, 7, 7};

Serial.println(arduboy.collision(r1, r2));








Running the above code will print 1 as the two rectangles overlap. The first rectangle’s top left corner is at 2, 2 and its lower right corner is at 6, 6. The second rectangle’s top left corner is at 5, 5 and its lower left corner is at 11, 11.

Moving the smaller rectangle away from the larger ones so they do not overlap:

r1.x = 0;
r1.y = 0;

Serial.println(arduboy.collision(r1, r2))








Running the above code will print 0 as the two rectangles do not overlap anymore. The first rectangle’s top left corner is now at 0, 0 and its lower right corner is at 4, 4. The second rectangle’s top left corner is at 5, 5 and its lower left corner is at 11, 11.

Using Collision Detection with Sprites

It is common to want to detect collisions between sprites - such as a player and an enemy. This often results in code like the following being scattered through a game's code to create Rects with the same position, width and height as the player and enemy sprites.

Rect player = { player.x, player.y, 16, 16 };
Rect enemy =  { enemy.x, enemy.y, 16, 16 };

Serial.println(arduboy.collide(player, enemy);

This is perfectly acceptable when there are only a handful of sprites to track - but what if there were dozens, and their sizes all different? The chance of coding errors increases significantly!

Assuming you are using classes to represent your players, enemies and other game play elements, a typical design pattern for action games is to define a class to hold information about players including their current position. A sample header and implementation class is shown below

Player.h:

#include "Arduboy2.h"

class Player {

   public:
     Rect getRect();
     
   int16_t x;
   int16_t y;
   const uint8_t *bitmap;
   
};



Player.cpp:

#include "Player.h"
#include "Arduboy2.h"

Rect Player::getRect() {
   
  return (Rect){x, y, pgm_read_byte(bitmap), pgm_read_byte(bitmap + 1)};

}

The header declares three public variables for the player’s coordinates and the image used when rendering the screen. It also prototypes a function that will return a rectangle structure that will describe the area on screen that the player currently occupies. The implementation of that function is included in the .cpp file. It simply creates and returns a populated Rect structure using the ‘x’ and ‘y’ coordinates from the class itself and the width and height of the image by reading these from the image array itself which happen to be the first two bytes when using the standard Sprites library. If you are unfamiliar with how sprite data is stored refer here.

The code below shows the declaration of two player objects and shows how they can be used when testing collisions.

Player playerA;
Player playerB;

playerA.x = 20;
playerA.y = 12;
playerA.bitmap = squareA;

playerB.x = 100;
playerB.y = 24;
playerB.bitmap = squareB;

...

Serial.print("A collides with B : ");
Serial.println(arduboy.collide(playerA.getRect(), playerB.getRect()));

In my opinion, this approach results in much more readable code.

The simple collision application and the ‘advanced’ version using a ‘Player’ class can be found in my repository.

https://github.com/filmote/Collide_Article1 (simple example)

https://github.com/filmote/Collide_Article2 (using the ‘Player’ class).