Sliding Collision
Eric Johnson

Hi there. I'm attempting to implement "sliding collision"--collision where, if you hold down and right while colliding with a wall to the right, you'll stop moving right, but will continue moving (or "sliding") down. This kind of collision is seen in many top-down 2D games (like A Link to the Past and other Zelda games). I'm not sure what this type of collision is actually called, but I'm going with "sliding collision" for now.

Here's a (poorly drawn) visualization of what I mean:

{"name":"collision_visualized.png","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/c\/2c09512ab285f9de23c8767e683eb94d.png","w":800,"h":286,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/2\/c\/2c09512ab285f9de23c8767e683eb94d"}

The yellow square is the player, moving down and to the right. The player then collides with a solid wall (the red square). While colliding, down and right are still being held, but collision is cancelled on the right, and movement continues downward. Finally, when the player is below the red wall, movement down and to the right resumes as expected.

Here's what I have already working with a single player and a single wall (the colors and outlines are different in the game than in the above visualization):

1#include <cmath> 2#include <iostream> 3 4#include <allegro5/allegro.h> 5#include <allegro5/allegro_primitives.h> 6 7using std::abs; 8using std::cout; 9 10void checkReturn(const bool condition) { 11 12 if (!condition) { 13 14 cout << "Error: something failed.\n"; 15 16 exit(EXIT_FAILURE); 17 } 18} 19 20bool isColliding(int ax, int ay, int ah, int aw, int bx, int by, int bw, int bh) { 21 22 return (abs(ax - bx) * 2 < (aw + bw)) && (abs(ay - by) * 2 < (ah + bh)); 23} 24 25int main(void) { 26 27 checkReturn(al_init()); 28 29 ALLEGRO_TIMER *timer; 30 ALLEGRO_DISPLAY *display; 31 ALLEGRO_EVENT_QUEUE *event_queue; 32 33 checkReturn((timer = al_create_timer(1.0 / 60.0))); 34 checkReturn((display = al_create_display(800, 450))); 35 checkReturn((event_queue = al_create_event_queue())); 36 37 checkReturn(al_install_keyboard()); 38 checkReturn(al_init_primitives_addon()); 39 40 al_register_event_source(event_queue, al_get_keyboard_event_source()); 41 al_register_event_source(event_queue, al_get_timer_event_source(timer)); 42 al_register_event_source(event_queue, al_get_display_event_source(display)); 43 44 al_start_timer(timer); 45 46 bool render = true; 47 bool running = true; 48 49 bool key[4] = {false, false, false, false}; 50 51 // Player variables. 52 int p_x = 0; // X axis 53 int p_y = 0; // Y axis 54 int p_w = 64; // Width 55 int p_h = 64; // Height 56 int p_s = 4; // Speed 57 58 // Wall variables (same as above). 59 int w_x = 256; 60 int w_y = 256; 61 int w_w = 64; 62 int w_h = 64; 63 64 enum {UP = 0, DOWN, LEFT, RIGHT}; 65 66 while (running) { 67 68 ALLEGRO_EVENT event; 69 70 al_wait_for_event(event_queue, &event); 71 72 if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) { 73 74 running = false; 75 } 76 else if (event.type == ALLEGRO_EVENT_KEY_DOWN) { 77 78 switch (event.keyboard.keycode) { 79 80 case ALLEGRO_KEY_UP: 81 82 key[UP] = true; 83 break; 84 85 case ALLEGRO_KEY_DOWN: 86 87 key[DOWN] = true; 88 break; 89 90 case ALLEGRO_KEY_LEFT: 91 92 key[LEFT] = true; 93 break; 94 95 case ALLEGRO_KEY_RIGHT: 96 97 key[RIGHT] = true; 98 break; 99 100 case ALLEGRO_KEY_ESCAPE: 101 102 running = false; 103 break; 104 } 105 } 106 else if (event.type == ALLEGRO_EVENT_KEY_UP) { 107 108 switch (event.keyboard.keycode) { 109 110 case ALLEGRO_KEY_UP: 111 112 key[UP] = false; 113 break; 114 115 case ALLEGRO_KEY_DOWN: 116 117 key[DOWN] = false; 118 break; 119 120 case ALLEGRO_KEY_LEFT: 121 122 key[LEFT] = false; 123 break; 124 125 case ALLEGRO_KEY_RIGHT: 126 127 key[RIGHT] = false; 128 break; 129 } 130 } 131 else if (event.type == ALLEGRO_EVENT_TIMER) { 132 133 render = true; 134 135 if (key[UP]) { 136 137 p_y -= p_s; 138 139 while (isColliding(p_x, p_y, p_w, p_h, w_x, w_y, w_w, w_h)) { 140 141 ++p_y; 142 } 143 } 144 145 if (key[DOWN]) { 146 147 p_y += p_s; 148 149 while (isColliding(p_x, p_y, p_w, p_h, w_x, w_y, w_w, w_h)) { 150 151 --p_y; 152 } 153 } 154 155 if (key[LEFT]) { 156 157 p_x -= p_s; 158 159 while (isColliding(p_x, p_y, p_w, p_h, w_x, w_y, w_w, w_h)) { 160 161 ++p_x; 162 } 163 } 164 165 if (key[RIGHT]) { 166 167 p_x += p_s; 168 169 while (isColliding(p_x, p_y, p_w, p_h, w_x, w_y, w_w, w_h)) { 170 171 --p_x; 172 } 173 } 174 } 175 176 if (render && al_is_event_queue_empty(event_queue)) { 177 178 render = false; 179 180 al_clear_to_color(al_map_rgb(255, 255, 255)); 181 182 // Draw player rectangle and outline. 183 al_draw_filled_rectangle(p_x, p_y, p_x + p_w, p_y + p_h, al_map_rgb(255, 255, 0)); 184 al_draw_rectangle(p_x + 4, p_y + 4, p_x + p_w - 4, p_y + p_h - 4, al_map_rgb(255, 0, 0), 8); 185 186 // Draw wall rectangle and outline. 187 al_draw_filled_rectangle(w_x, w_y, w_x + w_w, w_y + w_h, al_map_rgb(0, 255, 0)); 188 al_draw_rectangle(w_x + 4, w_y + 4, w_x + w_w - 4, w_y + w_h - 4, al_map_rgb(0, 0, 255), 8); 189 190 al_flip_display(); 191 } 192 } 193 194 // Free memory. 195 al_destroy_timer(timer); 196 al_destroy_display(display); 197 al_destroy_event_queue(event_queue); 198 199 return 0; 200}

The above works as expected. But now when I make multiples of the wall, things get a bit more complicated.

1#include <cmath> 2#include <iostream> 3 4#include <allegro5/allegro.h> 5#include <allegro5/allegro_primitives.h> 6 7using std::abs; 8using std::cout; 9 10void checkReturn(const bool condition) { 11 12 if (!condition) { 13 14 cout << "Error: something failed.\n"; 15 16 exit(EXIT_FAILURE); 17 } 18} 19 20bool isColliding(int ax, int ay, int ah, int aw, int bx, int by, int bw, int bh) { 21 22 return (abs(ax - bx) * 2 < (aw + bw)) && (abs(ay - by) * 2 < (ah + bh)); 23} 24 25int main(void) { 26 27 checkReturn(al_init()); 28 29 ALLEGRO_TIMER *timer; 30 ALLEGRO_DISPLAY *display; 31 ALLEGRO_EVENT_QUEUE *event_queue; 32 33 checkReturn((timer = al_create_timer(1.0 / 60.0))); 34 checkReturn((display = al_create_display(800, 450))); 35 checkReturn((event_queue = al_create_event_queue())); 36 37 checkReturn(al_install_keyboard()); 38 checkReturn(al_init_primitives_addon()); 39 40 al_register_event_source(event_queue, al_get_keyboard_event_source()); 41 al_register_event_source(event_queue, al_get_timer_event_source(timer)); 42 al_register_event_source(event_queue, al_get_display_event_source(display)); 43 44 al_start_timer(timer); 45 46 bool render = true; 47 bool running = true; 48 49 bool key[4] = {false, false, false, false}; 50 51 // Player variables. 52 int p_x = 0; // X axis 53 int p_y = 0; // Y axis 54 int p_w = 64; // Width 55 int p_h = 64; // Height 56 int p_s = 4; // Speed 57 58 // Wall variables (same as above). 59 int number_of_walls = 1; 60 int w_x[number_of_walls] = {256}; 61 int w_y[number_of_walls] = {256}; 62 int w_w[number_of_walls] = {64}; 63 int w_h[number_of_walls] = {64}; 64 65 enum {UP = 0, DOWN, LEFT, RIGHT}; 66 67 while (running) { 68 69 ALLEGRO_EVENT event; 70 71 al_wait_for_event(event_queue, &event); 72 73 if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) { 74 75 running = false; 76 } 77 else if (event.type == ALLEGRO_EVENT_KEY_DOWN) { 78 79 switch (event.keyboard.keycode) { 80 81 case ALLEGRO_KEY_UP: 82 83 key[UP] = true; 84 break; 85 86 case ALLEGRO_KEY_DOWN: 87 88 key[DOWN] = true; 89 break; 90 91 case ALLEGRO_KEY_LEFT: 92 93 key[LEFT] = true; 94 break; 95 96 case ALLEGRO_KEY_RIGHT: 97 98 key[RIGHT] = true; 99 break; 100 101 case ALLEGRO_KEY_ESCAPE: 102 103 running = false; 104 break; 105 } 106 } 107 else if (event.type == ALLEGRO_EVENT_KEY_UP) { 108 109 switch (event.keyboard.keycode) { 110 111 case ALLEGRO_KEY_UP: 112 113 key[UP] = false; 114 break; 115 116 case ALLEGRO_KEY_DOWN: 117 118 key[DOWN] = false; 119 break; 120 121 case ALLEGRO_KEY_LEFT: 122 123 key[LEFT] = false; 124 break; 125 126 case ALLEGRO_KEY_RIGHT: 127 128 key[RIGHT] = false; 129 break; 130 } 131 } 132 else if (event.type == ALLEGRO_EVENT_TIMER) { 133 134 render = true; 135 136 if (key[UP]) { 137 138 p_y -= p_s; 139 } 140 141 if (key[DOWN]) { 142 143 p_y += p_s; 144 } 145 146 if (key[LEFT]) { 147 148 p_x -= p_s; 149 } 150 151 if (key[RIGHT]) { 152 153 p_x += p_s; 154 } 155 156 for (int i = 0; i < number_of_walls; ++i) { 157 158 while (isColliding(p_x, p_y, p_w, p_h, w_x[i], w_y[i], w_w[i], w_h[i])) { 159 160 if (key[UP]) { 161 162 ++p_y; 163 } 164 165 if (key[DOWN]) { 166 167 --p_y; 168 } 169 170 if (key[LEFT]) { 171 172 ++p_x; 173 } 174 175 if (key[RIGHT]) { 176 177 --p_x; 178 } 179 } 180 } 181 } 182 183 if (render && al_is_event_queue_empty(event_queue)) { 184 185 render = false; 186 187 al_clear_to_color(al_map_rgb(255, 255, 255)); 188 189 // Draw player rectangle and outline. 190 al_draw_filled_rectangle(p_x, p_y, p_x + p_w, p_y + p_h, al_map_rgb(255, 255, 0)); 191 al_draw_rectangle(p_x + 4, p_y + 4, p_x + p_w - 4, p_y + p_h - 4, al_map_rgb(255, 0, 0), 8); 192 193 al_hold_bitmap_drawing(true); 194 195 for (int i = 0; i < number_of_walls; ++i) { 196 197 // Draw wall rectangle and outline. 198 al_draw_filled_rectangle(w_x[i], w_y[i], w_x[i] + w_w[i], w_y[i] + w_h[i], al_map_rgb(0, 255, 0)); 199 al_draw_rectangle(w_x[i] + 4, w_y[i] + 4, w_x[i] + w_w[i] - 4, w_y[i] + w_h[i] - 4, al_map_rgb(0, 0, 255), 8); 200 } 201 202 al_hold_bitmap_drawing(false); 203 204 al_flip_display(); 205 } 206 } 207 208 // Free memory. 209 al_destroy_timer(timer); 210 al_destroy_display(display); 211 al_destroy_event_queue(event_queue); 212 213 return 0; 214}

I made the wall variables an array. Drawing works no problem, but collision doesn't work as expected. I limited the number of walls to just 1, to test it against my earlier code, but now the player collides with a wall and just becomes "stuck"; it doesn't "slide" like I expected. What am I doing wrong?

I haven't done collision like this before, so I'm surely making mistakes, but I appreciate any and all feedback and suggestions. Thank you!

Edit
I figured it out. I moved the for loop within each of the if statements, and now it works:

1#include <cmath> 2#include <iostream> 3 4#include <allegro5/allegro.h> 5#include <allegro5/allegro_primitives.h> 6 7using std::abs; 8using std::cout; 9 10void checkReturn(const bool condition) { 11 12 if (!condition) { 13 14 cout << "Error: something failed.\n"; 15 16 exit(EXIT_FAILURE); 17 } 18} 19 20bool isColliding(int ax, int ay, int ah, int aw, int bx, int by, int bw, int bh) { 21 22 return (abs(ax - bx) * 2 < (aw + bw)) && (abs(ay - by) * 2 < (ah + bh)); 23} 24 25int main(void) { 26 27 checkReturn(al_init()); 28 29 ALLEGRO_TIMER *timer; 30 ALLEGRO_DISPLAY *display; 31 ALLEGRO_EVENT_QUEUE *event_queue; 32 33 checkReturn((timer = al_create_timer(1.0 / 60.0))); 34 checkReturn((display = al_create_display(800, 450))); 35 checkReturn((event_queue = al_create_event_queue())); 36 37 checkReturn(al_install_keyboard()); 38 checkReturn(al_init_primitives_addon()); 39 40 al_register_event_source(event_queue, al_get_keyboard_event_source()); 41 al_register_event_source(event_queue, al_get_timer_event_source(timer)); 42 al_register_event_source(event_queue, al_get_display_event_source(display)); 43 44 al_start_timer(timer); 45 46 bool render = true; 47 bool running = true; 48 49 bool key[4] = {false, false, false, false}; 50 51 // Player variables. 52 int p_x = 0; // X axis 53 int p_y = 0; // Y axis 54 int p_w = 64; // Width 55 int p_h = 64; // Height 56 int p_s = 4; // Speed 57 58 int number_of_walls = 2; 59 60 // Wall variables (same as above). 61 int w_x[number_of_walls] = {256, 64 * 6}; 62 int w_y[number_of_walls] = {256, 256}; 63 int w_w[number_of_walls] = {64, 64}; 64 int w_h[number_of_walls] = {64, 64}; 65 66 enum {UP = 0, DOWN, LEFT, RIGHT}; 67 68 while (running) { 69 70 ALLEGRO_EVENT event; 71 72 al_wait_for_event(event_queue, &event); 73 74 if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) { 75 76 running = false; 77 } 78 else if (event.type == ALLEGRO_EVENT_KEY_DOWN) { 79 80 switch (event.keyboard.keycode) { 81 82 case ALLEGRO_KEY_UP: 83 84 key[UP] = true; 85 break; 86 87 case ALLEGRO_KEY_DOWN: 88 89 key[DOWN] = true; 90 break; 91 92 case ALLEGRO_KEY_LEFT: 93 94 key[LEFT] = true; 95 break; 96 97 case ALLEGRO_KEY_RIGHT: 98 99 key[RIGHT] = true; 100 break; 101 102 case ALLEGRO_KEY_ESCAPE: 103 104 running = false; 105 break; 106 } 107 } 108 else if (event.type == ALLEGRO_EVENT_KEY_UP) { 109 110 switch (event.keyboard.keycode) { 111 112 case ALLEGRO_KEY_UP: 113 114 key[UP] = false; 115 break; 116 117 case ALLEGRO_KEY_DOWN: 118 119 key[DOWN] = false; 120 break; 121 122 case ALLEGRO_KEY_LEFT: 123 124 key[LEFT] = false; 125 break; 126 127 case ALLEGRO_KEY_RIGHT: 128 129 key[RIGHT] = false; 130 break; 131 } 132 } 133 else if (event.type == ALLEGRO_EVENT_TIMER) { 134 135 render = true; 136 137 if (key[UP]) { 138 139 p_y -= p_s; 140 141 for (int i = 0; i < number_of_walls; ++i) { 142 143 while (isColliding(p_x, p_y, p_w, p_h, w_x[i], w_y[i], w_w[i], w_h[i])) { 144 145 ++p_y; 146 } 147 } 148 } 149 150 if (key[DOWN]) { 151 152 p_y += p_s; 153 154 for (int i = 0; i < number_of_walls; ++i) { 155 156 while (isColliding(p_x, p_y, p_w, p_h, w_x[i], w_y[i], w_w[i], w_h[i])) { 157 158 --p_y; 159 } 160 } 161 } 162 163 if (key[LEFT]) { 164 165 p_x -= p_s; 166 167 for (int i = 0; i < number_of_walls; ++i) { 168 169 while (isColliding(p_x, p_y, p_w, p_h, w_x[i], w_y[i], w_w[i], w_h[i])) { 170 171 ++p_x; 172 } 173 } 174 } 175 176 if (key[RIGHT]) { 177 178 p_x += p_s; 179 180 for (int i = 0; i < number_of_walls; ++i) { 181 182 while (isColliding(p_x, p_y, p_w, p_h, w_x[i], w_y[i], w_w[i], w_h[i])) { 183 184 --p_x; 185 } 186 } 187 } 188 } 189 190 if (render && al_is_event_queue_empty(event_queue)) { 191 192 render = false; 193 194 al_clear_to_color(al_map_rgb(255, 255, 255)); 195 196 // Draw player rectangle and outline. 197 al_draw_filled_rectangle(p_x, p_y, p_x + p_w, p_y + p_h, al_map_rgb(255, 255, 0)); 198 al_draw_rectangle(p_x + 4, p_y + 4, p_x + p_w - 4, p_y + p_h - 4, al_map_rgb(255, 0, 0), 8); 199 200 for (int i = 0; i < number_of_walls; ++i) { 201 202 // Draw wall rectangle and outline. 203 al_draw_filled_rectangle(w_x[i], w_y[i], w_x[i] + w_w[i], w_y[i] + w_h[i], al_map_rgb(0, 255, 0)); 204 al_draw_rectangle(w_x[i] + 4, w_y[i] + 4, w_x[i] + w_w[i] - 4, w_y[i] + w_h[i] - 4, al_map_rgb(0, 0, 255), 8); 205 } 206 207 al_flip_display(); 208 } 209 } 210 211 // Free memory. 212 al_destroy_timer(timer); 213 al_destroy_display(display); 214 al_destroy_event_queue(event_queue); 215 216 return 0; 217}

It's not pretty, but it works. I've solved my own question now, but if you know of a way to improve the above code to make it more succinct, I'd appreciate you telling me. Thanks!