Skip to main content

Command Palette

Search for a command to run...

Enemies, Spawning, and Falling Objects

Published
7 min read
Enemies, Spawning, and Falling Objects
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.

Up to now:

  • we can move the cannon

  • we can shoot bullets

  • bullets move and disappear correctly

But there’s still one big problem.

There is nothing to shoot at.

In this part, we’ll fix that by introducing enemies that appear at the top of the screen and fall down over time.

By the end of this part:

  • enemies will spawn at random positions

  • they will move downward every frame

  • the screen will no longer feel empty

No collisions yet. One step at a time.

What Is an Enemy (Conceptually)?

Just like bullets, an enemy is very simple.

An enemy has:

  • a position (x, y)

  • a downward movement

That’s it.

No AI. No pathfinding.
Just something that exists and moves.

Representing an Enemy in Code

We’ll start with a small struct.

struct Enemy {
  int x;
  int y;
};

Simple, readable, and enough for now.

Storing Multiple Enemies

Just like bullets, we’ll need more than one enemy.

#include <vector>

std::vector<Enemy> enemies;

This lets us:

  • spawn enemies over time

  • update all enemies every frame

  • remove them later when needed

Spawning Enemies

Enemies should not all appear at once.
They should spawn periodically.

The simplest way to do this is with a counter.

int spawnCounter = 0;
const int SPAWN_DELAY = 60; // frames

At 60 FPS, this means:

  • one enemy every second

Inside the game loop:

spawnCounter++;

if (spawnCounter >= SPAWN_DELAY) {
  int enemyX = rand() % maxX;
  enemies.push_back({ enemyX, 1 });
  spawnCounter = 0;
}

This spawns an enemy near the top of the screen at a random horizontal position.

Moving Enemies Downward

Enemies fall down by increasing their y value.

for (auto &enemy : enemies) {
  enemy.y++;
}

Every frame, enemies move one row closer to the bottom.

Simple and effective.

Rendering Enemies

Rendering enemies is just another loop.

for (const auto &enemy : enemies) {
  mvaddch(enemy.y, enemy.x, 'V');
}

Now enemies are visible and moving.

Removing Enemies That Leave the Screen

If an enemy reaches the bottom, it should be removed.

For now, we’ll just delete it.

for (size_t i = 0; i < enemies.size(); ) {
  if (enemies[i].y >= maxY) {
    enemies.erase(enemies.begin() + i);
  } else {
    i++;
  }
}

Later, we’ll turn this into a game over condition.

Full Working Example (Player + Bullets + Enemies)

Here’s the complete version so far.

#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() {
  initscr();
  cbreak();
  noecho();
  nodelay(stdscr, TRUE);
  keypad(stdscr, TRUE);
  curs_set(0);

  int maxY, maxX;
  getmaxyx(stdscr, maxY, maxX);

  int playerX = maxX / 2;
  int playerY = maxY - 2;

  std::vector<Bullet> bullets;
  std::vector<Enemy> enemies;

  bool running = true;

  const int FPS = 60;
  const int FRAME_TIME = 1000 / FPS;

  int spawnCounter = 0;
  const int SPAWN_DELAY = 60;

  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;
    }

    // -------- 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 --------
    for (auto &e : enemies) {
      e.y++;
    }

    for (size_t i = 0; i < enemies.size(); ) {
      if (enemies[i].y >= maxY) {
        enemies.erase(enemies.begin() + i);
      } else {
        i++;
      }
    }

    // -------- Render --------
    clear();
    mvaddch(playerY, playerX, 'A');

    for (const auto &b : bullets) {
      mvaddch(b.y, b.x, '|');
    }

    for (const auto &e : enemies) {
      mvaddch(e.y, e.x, 'V');
    }

    mvprintw(0, 0, "LEFT/RIGHT move | SPACE shoot | Q quit");
    refresh();

    // -------- Timing --------
    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;
}

Go ahead, run this code.

You probably noticed it too, right?
The enemies are moving way too fast.

So how do we slow them down?

Slowing Down the Enemies

Right now, enemies are moving every frame, which makes them feel more like meteors than aliens.
That’s not what we want.
To fix this, we’ll introduce a simple trick.

Instead of moving enemies every frame, we’ll move them once every x frames.
If:

  • x is large, enemies move slower

  • x is small, enemies move faster

We’ll call this value inverseEnemySpeed.

At the moment, its value is 1, which means enemies move on every single frame.
That’s why they feel so fast.

How This Works

We already have a game loop running at a fixed FPS, so now we keep track of how many frames have passed using frameCount.

We’ll use this frame count to decide when enemies are allowed to move.

unsigned int frameCount = 0;
int inverseEnemySpeed = 10;

Inside the game loop:

frameCount++;

if (frameCount % inverseEnemySpeed == 0) {
  for (auto &enemy : enemies) {
    enemy.y++;
  }
}

What this does is simple.

The game loop still runs every frame, but enemies only move once every inverseEnemySpeed frames.

As a result:

  • the game loop remains smooth

  • enemies move at a controlled pace

  • enemy speed can be adjusted by changing a single variable

No timers. No extra state. No unnecessary complexity.

Full Working Code (With Slower Enemies)

#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;

  // -------- Timing --------
  const int FPS = 60;
  const int FRAME_TIME = 1000 / FPS;

  // -------- Enemy spawning --------
  int spawnCounter = 0;
  const int SPAWN_DELAY = 60; // one enemy per second

  // -------- Enemy movement control --------
  unsigned int frameCount = 0;
  int inverseEnemySpeed = 10; // higher = slower enemies

  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 (slowed) --------
    frameCount++;

    if (frameCount % inverseEnemySpeed == 0) {
      for (auto &e : enemies) {
        e.y++;
      }
    }

    for (size_t i = 0; i < enemies.size(); ) {
      if (enemies[i].y >= maxY) {
        enemies.erase(enemies.begin() + i);
      } else {
        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');
    }

    mvprintw(0, 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;
}

Go ahead and tweak the values according to you!

Why This Part Is Huge

You’ve now implemented:

  • timed spawning

  • multiple enemy management

  • downward movement

  • a living game world

The game finally has pressure.

What’s Next

In the next part, we’ll add:

  • bullet–enemy collision

  • enemy destruction

  • scoring

That’s when the loop finally closes.

Terminal Game Development with C++ and ncurses

Part 6 of 10

Build a real-time terminal game using C++ and ncurses, starting from the basics and ending with a playable mini-game. Learn input handling, game loops, rendering, and core game logic without using graphics engines.

Up next

Collision Detection and Scoring

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