Enemies, Spawning, and Falling Objects

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.



