Collision Detection and Scoring

An engineer with a curious mind and a love for systems that go beyond the surface.I enjoy exploring how things work under the hood — whether it's rendering pixels, routing audio, or handling low-level protocols. My work spans across game development, system programming, and occasionally web tech when the problem is worth solving. I’m driven by the desire to build things that are fast, efficient, and just a bit unconventional. This blog is my space to document experiments, challenges, and the weirdly satisfying bugs that teach you more than tutorials ever could.
Outside of tech, I enjoy observing how people use technology, and occasionally breaking things just to fix them better.
So far, bullets and enemies happily pass through each other like ghosts.
That’s not very exciting.
In this part, we’ll make bullets actually hit enemies.
By the end of this part:
bullets will destroy enemies
enemies will disappear on hit
the game will finally have a score
No physics engine. No math overload.
Just clean, understandable logic.
What Is a Collision (In Our Game)?
In our case, a collision is very simple.
A bullet hits an enemy if both occupy the same cell.
That means:
bullet.x == enemy.x &&
bullet.y == enemy.y
That’s it.
We’re working with characters, not pixels, so this works perfectly.
Where Collision Logic Belongs
Collision checks should happen after updates, but before rendering.
Why?
positions must be up to date
rendering should reflect the final state of the frame
So the order becomes:
Input
Update bullets & enemies
Check collisions
Render
Detecting Bullet–Enemy Collisions
We’ll compare every bullet with every enemy.
Yes, it’s a nested loop.
Yes, it’s fine for this scale.
for (size_t i = 0; i < bullets.size(); ) {
bool bulletRemoved = false;
for (size_t j = 0; j < enemies.size(); ) {
if (bullets[i].x == enemies[j].x &&
bullets[i].y == enemies[j].y) {
// Collision detected
bullets.erase(bullets.begin() + i);
enemies.erase(enemies.begin() + j);
bulletRemoved = true;
break;
} else {
j++;
}
}
if (!bulletRemoved) {
i++;
}
}
This removes:
the bullet
the enemy it hit
Immediately.
Warning: Never use a standard for (auto &b : bullets) loop when deleting items inside that same loop. If you delete the 3rd item, the 4th item becomes the new 3rd item, and your loop will skip it! The manual index method we used ensures no enemy is ever skipped.
Adding a Score
Now let’s reward the player.
Add a score variable:
int score = 0;
Increment it when a collision happens:
score++;
Simple. Effective.
Displaying the Score
In the render section:
mvprintw(0, 0, "Score: %d", score);
Now the player has feedback.
Now we are getting somewhere.
Full Working Code (With Collisions and Scoring)
#include <ncurses.h>
#include <chrono>
#include <thread>
#include <vector>
#include <cstdlib>
struct Bullet {
int x;
int y;
};
struct Enemy {
int x;
int y;
};
int main() {
// -------- ncurses setup --------
initscr();
cbreak();
noecho();
nodelay(stdscr, TRUE);
keypad(stdscr, TRUE);
curs_set(0);
int maxY, maxX;
getmaxyx(stdscr, maxY, maxX);
// -------- Player setup --------
int playerX = maxX / 2;
int playerY = maxY - 2;
std::vector<Bullet> bullets;
std::vector<Enemy> enemies;
bool running = true;
int score = 0;
// -------- Timing --------
const int FPS = 60;
const int FRAME_TIME = 1000 / FPS;
// -------- Enemy spawning --------
int spawnCounter = 0;
const int SPAWN_DELAY = 60;
// -------- Enemy movement control --------
int frameCount = 0;
int inverseEnemySpeed = 10;
while (running) {
auto start = std::chrono::high_resolution_clock::now();
// -------- Input --------
int ch = getch();
switch (ch) {
case 'q':
case 'Q':
running = false;
break;
case KEY_LEFT:
playerX--;
break;
case KEY_RIGHT:
playerX++;
break;
case ' ':
bullets.push_back({ playerX, playerY - 1 });
break;
default:
break;
}
// -------- Player bounds --------
if (playerX < 0) playerX = 0;
if (playerX >= maxX) playerX = maxX - 1;
// -------- Spawn enemies --------
spawnCounter++;
if (spawnCounter >= SPAWN_DELAY) {
enemies.push_back({ rand() % maxX, 1 });
spawnCounter = 0;
}
// -------- Update bullets --------
for (auto &b : bullets) {
b.y--;
}
for (size_t i = 0; i < bullets.size(); ) {
if (bullets[i].y < 0) {
bullets.erase(bullets.begin() + i);
} else {
i++;
}
}
// -------- Update enemies (slower movement) --------
frameCount++;
if (frameCount % inverseEnemySpeed == 0) {
for (auto &e : enemies) {
e.y++;
}
}
// -------- Bullet–Enemy collision --------
for (size_t i = 0; i < bullets.size(); ) {
bool bulletRemoved = false;
for (size_t j = 0; j < enemies.size(); ) {
if (bullets[i].x == enemies[j].x &&
bullets[i].y == enemies[j].y) {
bullets.erase(bullets.begin() + i);
enemies.erase(enemies.begin() + j);
score++;
bulletRemoved = true;
break;
} else {
j++;
}
}
if (!bulletRemoved) {
i++;
}
}
// -------- Render --------
clear();
// Player
mvaddch(playerY, playerX, 'A');
// Bullets
for (const auto &b : bullets) {
mvaddch(b.y, b.x, '|');
}
// Enemies
for (const auto &e : enemies) {
mvaddch(e.y, e.x, 'V');
}
// HUD
mvprintw(0, 0, "Score: %d", score);
mvprintw(1, 0, "LEFT/RIGHT move | SPACE shoot | Q quit");
refresh();
// -------- FPS control --------
auto end = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
if (elapsed < FRAME_TIME) {
std::this_thread::sleep_for(
std::chrono::milliseconds(FRAME_TIME - elapsed));
}
}
endwin();
return 0;
}
What We’ve Achieved
At this point, the game has:
player control
shooting
enemies
collisions
scoring
This is a complete gameplay loop.
Everything after this is polish and rules.
What’s Next
In the next part, we’ll add:
game over conditions
restart logic
Game States
That’s when it stops being a demo and becomes a real game.



