Loading...
Searching...
No Matches
Collision Detection Tutorial

This tutorial covers collision detection on the SNES, from basic bounding box checks to tile-based world collision used in platformers.

Why AABB?

The SNES has no hardware collision detection. All collision must be done in software by the CPU. Axis-aligned bounding box (AABB) testing is the standard approach because it requires only four comparisons per pair of objects – fast enough for the 65816 at 3.58 MHz.

The Rect Type

OpenSNES provides a Rect structure representing an axis-aligned bounding box:

#include <snes.h>
#include <snes/collision.h>
typedef struct {
s16 x; /* Left edge X coordinate */
s16 y; /* Top edge Y coordinate */
u16 width; /* Width in pixels */
u16 height; /* Height in pixels */
} Rect;
SNES Collision Detection.
signed short s16
16-bit signed integer (-32768 to 32767)
Definition types.h:49
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:52
OpenSNES Master Header.
Axis-aligned bounding box (rectangle)
Definition collision.h:54

Initialize it with rectInit() or by direct assignment:

static Rect player_box
Player bounding box for AABB collision tests via collideRect()
Definition main.c:59
static s16 player_y
Player Y position in screen coordinates.
Definition main.c:57
static s16 player_x
Player X position in screen coordinates.
Definition main.c:55
void rectInit(Rect *r, s16 x, s16 y, u16 w, u16 h)
Initialize a rectangle.

Sprite-to-Sprite Collision

Basic Overlap Test

Use collideRect() to test whether two rectangles overlap:

static void check_collisions(void) {
/* Collision detected */
player_hit();
}
}
static s16 enemy_y[4]
Y positions of the four static enemy sprites.
Definition main.c:64
static void check_collisions(void)
Test player-vs-enemy AABB overlap and update collision_flags.
Definition main.c:330
static Rect enemy_box[4]
Bounding boxes for each enemy, rebuilt every frame for collideRect()
Definition main.c:66
static s16 enemy_x[4]
X positions of the four static enemy sprites.
Definition main.c:62
u8 collideRect(Rect *a, Rect *b)
Check rectangle vs rectangle collision (AABB)
u16 height
Definition collision.h:58
s16 y
Definition collision.h:56
u16 width
Definition collision.h:57
s16 x
Definition collision.h:55

Testing Multiple Enemies

The collision_demo example checks the player against four enemies each frame using a bitmask to track which enemies are currently colliding:

#define NUM_ENEMIES 4
static void check_collisions(void) {
u8 i;
for (i = 0; i < NUM_ENEMIES; i++) {
collision_flags |= (1 << i);
}
}
}
static u8 collision_flags
Bitmask tracking which enemies the player is currently overlapping.
Definition main.c:74
#define PLAYER_SIZE
Definition main.c:49
#define NUM_ENEMIES
Definition main.c:51
#define ENEMY_SIZE
Definition main.c:50
static u8 i
Definition main.c:156
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:46

Point-vs-Rectangle

Use collidePoint() when checking a single coordinate (bullet, cursor) against a target:

if (collidePoint(bullet_x, bullet_y, &target_box)) {
target_hit();
}
u8 collidePoint(s16 x, s16 y, Rect *r)
Check point vs rectangle collision.

Sprite-to-Background Collision

Reading Tilemap Data

For platformers and top-down games, the world is made of tiles. A separate collision map (one byte per tile, 0 = empty, nonzero = solid) lets you check whether a pixel position is blocked:

#define MAP_WIDTH 16
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* Top wall */
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
/* ... */
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* Bottom wall */
};
static u8 collision_map[16 *14]
Tile-based collision map (16x14 = 224 bytes, 128x112 pixel area)
Definition main.c:94
#define MAP_HEIGHT
Definition main.c:81
#define MAP_WIDTH
Definition main.c:80

Use collideTile() to look up the tile at a pixel position:

/* Standing on solid ground */
on_ground = 1;
}
u8 collideTile(s16 px, s16 py, u8 *tilemap, u16 mapWidth)
Check collision with tile at pixel coordinates.

Checking All Four Corners

A single point check misses edges. The collision_demo example checks all four corners of the player bounding box to prevent clipping through walls:

static u8 check_wall_collision(s16 new_x, s16 new_y) {
s16 map_x, map_y;
map_x = new_x - MAP_OFFSET_X;
map_y = new_y - MAP_OFFSET_Y;
/* Top-left */
if (collideTile(map_x, map_y, collision_map, MAP_WIDTH)) return 1;
/* Top-right */
if (collideTile(map_x + PLAYER_SIZE - 1, map_y, collision_map, MAP_WIDTH)) return 1;
/* Bottom-left */
if (collideTile(map_x, map_y + PLAYER_SIZE - 1, collision_map, MAP_WIDTH)) return 1;
/* Bottom-right */
if (collideTile(map_x + PLAYER_SIZE - 1, map_y + PLAYER_SIZE - 1, collision_map, MAP_WIDTH)) return 1;
return 0;
}
#define MAP_OFFSET_Y
Screen Y offset where the collision map starts (centers the 112px map on 224px screen)
Definition main.c:114
#define MAP_OFFSET_X
Screen X offset where the collision map starts (centers the 128px map on 256px screen)
Definition main.c:112
static u8 check_wall_collision(s16 new_x, s16 new_y)
Test whether a position would overlap a solid wall tile.
Definition main.c:371

You can also use collideRectTile() which does this internally:

Rect playerBox = { map_x, map_y, PLAYER_SIZE, PLAYER_SIZE };
/* Player hit a solid tile */
}
u8 collideRectTile(Rect *r, u8 *tilemap, u16 mapWidth)
Check collision between rectangle and tilemap.

Custom Tile Sizes

For maps with 16x16 tiles, use collideTileEx():

on_ground = 1;
}
u8 collideTileEx(s16 px, s16 py, u8 *tilemap, u16 mapWidth, u8 tileSize)
Check collision with tile using custom tile size.

Tile Properties from a Tilemap (Platformer Pattern)

The likemario example uses 16-bit tile properties looked up from the visual tilemap, rather than a separate collision map. Each tile index maps to a property value (T_SOLID or T_EMPTY):

#define T_EMPTY 0x0000
#define T_SOLID 0xFF00
u16 tx, ty;
if (px < 0 || py < 0) return T_SOLID;
tx = (u16)px >> 3; /* Divide by 8 (tile width) */
ty = (u16)py >> 3;
if (tx >= map_width || ty >= map_height) return T_EMPTY;
return tile_props[map_row_ptrs[ty][tx] & 0x03FF];
}
static u16 px
Definition main.c:166
static u16 map_height
Definition main.c:139
static u16 map_get_tile_prop(s16 px, s16 py)
Look up the collision property of the tile at pixel coordinates.
Definition main.c:199
static u16 * tile_props
Definition main.c:141
static u16 map_width
Definition main.c:138
static u16 * map_row_ptrs[32]
Precomputed row pointers into map_data for fast tile lookup.
Definition main.c:150
#define T_EMPTY
Empty tile (object will fall through)
Definition map.h:54
#define T_SOLID
Solid tile (blocks movement)
Definition map.h:57

This avoids storing a duplicate collision map – the visual tilemap doubles as the collision source.

Collision Response Patterns

Detecting a collision is only half the problem. What happens next matters more.

Stopping Movement (Slide Along Walls)

When the player tries to move diagonally into a wall, test each axis independently so they can slide along the wall instead of getting stuck:

new_x = player_x;
new_y = player_y;
if (pad & KEY_LEFT) new_x -= PLAYER_SPEED;
if (pad & KEY_RIGHT) new_x += PLAYER_SPEED;
if (pad & KEY_UP) new_y -= PLAYER_SPEED;
if (pad & KEY_DOWN) new_y += PLAYER_SPEED;
if (!check_wall_collision(new_x, new_y)) {
/* No collision -- accept full movement */
player_x = new_x;
player_y = new_y;
} else {
/* Try each axis separately */
player_x = new_x; /* Horizontal OK */
}
player_y = new_y; /* Vertical OK */
}
}
#define PLAYER_SPEED
Definition main.c:52
#define KEY_RIGHT
Definition input.h:78
#define KEY_DOWN
Definition input.h:76
#define KEY_LEFT
Definition input.h:77
#define KEY_UP
Definition input.h:75

Pushing Out of Overlap

Use collideRectEx() to get the overlap amount and push objects apart. This is useful for objects that can drift into each other (e.g., physics objects):

s16 dx, dy;
if (collideRectEx(&player, &wall, &dx, &dy)) {
player_x -= dx;
player_y -= dy;
}
u8 collideRectEx(Rect *a, Rect *b, s16 *overlapX, s16 *overlapY)
Check if two rectangles overlap and return overlap amount.

Bouncing (Breakout Pattern)

The breakout example reverses velocity on collision with walls and paddle. Negation uses a temporary variable to work around compiler constraints:

/* Wall bounce */
if (pos_x > 171) {
s16 neg_vx = -vel_x;
vel_x = neg_vx;
pos_x = 171;
}
/* Paddle bounce with angle control */
if (pos_y > 195 && pos_y < 203) {
if (pos_x >= px && pos_x <= px + 27) {
u8 zone = (pos_x - px) / 7;
switch (zone) {
case 0: vel_x = -2; vel_y = -1; break;
case 1: vel_x = -1; vel_y = -2; break;
case 2: vel_x = 1; vel_y = -2; break;
default: vel_x = 2; vel_y = -1; break;
}
}
}
static s16 vel_y
Definition main.c:176
static s16 pos_x
Definition main.c:177
static s16 pos_y
Definition main.c:178
static s16 vel_x
Ball X velocity (-2 to +2 pixels/frame).
Definition main.c:175

Snapping to Tile Grid (Platformer Landing)

When a falling character hits the ground, snap their Y position to the tile boundary to prevent sinking into the floor:

/* Ground check: test two points at the bottom edge */
prop = map_get_tile_prop(mario_x + 2, mario_y + 16);
if (prop == T_EMPTY)
prop = map_get_tile_prop(mario_x + 13, mario_y + 16);
if (prop != T_EMPTY) {
/* Snap to tile boundary */
mario_y = ((mario_y + 16) & 0xFFF8) - 16;
on_ground = 1;
}
static s16 mario_yvel
Definition main.c:160
static s16 mario_x
Definition main.c:155
static s16 mario_y
Definition main.c:156

The mask 0xFFF8 rounds down to the nearest 8-pixel tile boundary. Subtracting the sprite height (16) places the sprite so its feet rest exactly on top of the solid tile.

Grid-Based Collision (Breakout Bricks)

The breakout example converts the ball's pixel position to a grid coordinate and checks a brick array directly:

/* Convert pixel to brick grid (16x8 pixel bricks) */
bx = (pos_x - 14) >> 4; /* Divide by 16 (brick width) */
by = (pos_y - 14) >> 3; /* Divide by 8 (brick height) */
if (bx < 10 && by >= 1 && by <= 10) {
u16 idx = bx + (by << 3) + (by << 1) - 10;
if (blocks[idx] != 8) {
/* Brick hit -- remove it and bounce */
}
}
static void remove_brick(void)
Remove a brick and update score.
Definition main.c:642
static u16 bx
Definition main.c:159
static u16 by
Definition main.c:159
u8 blocks[]
Mutable brick state array (100 entries, one per grid cell).

Performance Tips

  1. Check collision only when objects are close. Skip collideRect() for enemies off screen.
  2. Use bitmasks for groups. Track collision state with bit flags (collision_flags |= (1 << i)) instead of boolean arrays.
  3. Separate axes. Test X and Y independently – this is cheaper than diagonal resolution and gives better sliding behavior.
  4. Power-of-2 tile sizes. Use 8 or 16 pixel tiles so division becomes a bit shift (>> 3 or >> 4).
  5. Avoid collideRect() in tight loops with many objects. For N-vs-N collision, consider spatial partitioning (grid cells) to reduce the number of pairs tested.

Complete Examples

  • examples/basics/collision_demo/ – AABB sprite collision with tile-based wall collision and visual feedback
  • examples/games/breakout/ – Ball vs walls, ball vs paddle (angle control), ball vs brick grid
  • examples/games/likemario/ – Platformer tile collision with gravity, ground snapping, and wall sliding

Next Steps