Skip to main content

Command Palette

Search for a command to run...

Collision Detection and Scoring

Published
4 min read
Collision Detection and Scoring
D

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:

  1. Input

  2. Update bullets & enemies

  3. Check collisions

  4. 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.

Collision Detection & Scoring Simplified