Logo Glusoft

How to make a bullet hell game

bullet hell game

Setting up the project

In this tutorial we will use SFML and for the bullets we need a sprites sheet:
Right click on the picture, save... to download the images
sprites of bullets

For the bullet the coordinates of each image are defined here:

<a n="sprites">
	<f f="bigRed" tx="0" ty="0" tw="64" th="64"/>
	<f f="smallRed" tx="0" ty="64" tw="16" th="16"/>
	<f f="bigGreen" tx="64" ty="0" tw="64" th="64"/>
	<f f="bigBlue" tx="128" ty="0" tw="64" th="64"/>
	<f f="smallPurple" tx="0" ty="80" tw="8" th="8"/>

And also the sprite for the player:

Create a class for the bullets

We will need to have a class for the bullet, the class is very short so everything is defined in the header file:

class Bullet {
	Bullet(sf::Texture *tex, sf::Vector2f vel, sf::Vector2f pos, sf::IntRect rect) {
		this->vel = vel;

	// Update the position of the bullet
	void update(double timeElapsed) {
		spr.setPosition(spr.getPosition().x + timeElapsed * vel.x, spr.getPosition().y + 
		timeElapsed * vel.y);

	sf::Sprite* getSprite() {
		return &spr;

	sf::Sprite spr;
	sf::Vector2f vel;

For the moment maybe you don't understand the point of having the velocity stored, and what is the parameter double timeElapsed in the function update.
As you can remember from school to calulate the velocity:
velocity of an object = distance / (t2 - t1)
and by definition timeElapsed = (t2 - t1) 

=> distance = velocity * timeElapsed

Isn't it more simple to use the distance ?
The distance depend on the timeElapsed and the timeElapsed is variable (by default the framerate is variable). To make the timeElapsed constant we need to enable the vertical synchronization (set the framerate at 60fps).

But in this case we do not want to limit the framerate because we have a lot of bullets to render, so having a very high framerate will allow the program to have a margin to absorb the loss due to the rendering. To sum up the rendering will appear smoother in some cases if the framerate is variable. In the screenshot of the game you can see the framerate is at 326.477295 fps.


Create the window

Later we will use Pi to convert degree to radian:

const double Pi = 3.14159265358979323846;

const size_t winW = 1366;
const size_t winH = 768;

sf::RenderWindow window(sf::VideoMode(winW, winH), "BulletHell");

Loading of textures

// Create an array to hold the bullets
const size_t maxBullets = 3000;
const size_t totalBullets = 16 * maxBullets;

Bullet* bullets[totalBullets];
for (size_t i = 0; i < totalBullets; i++) {
	bullets[i] = NULL;

// Use a vertex array for the rendering of the bullets
sf::VertexArray vertices(sf::Quads, 4 * totalBullets);

// Load the texture of the bullets
sf::Texture tex;

// Load the texture of the player
sf::Texture playerTex;

// Create the sprite for the player
sf::Sprite player(playerTex);
player.setPosition(winW / 2, winH - player.getTextureRect().height);
double velPlayer = 0.1;

// Create variables for the bullets patterns
double counterTime = 0;
double counterTime2 = 0;
double counterTime3 = 0;

double counterWave = 0;
double counterWave2 = 0;
double counterWave3 = 0;

double bulletTime = 0.1;
double bulletTime2 = 40;
double bulletTime3 = 100;

size_t numBullet = 0;
size_t numBullet2 = 0;
size_t numBullet3 = 0;

// Load font for the framerate
sf::Font font;

// Create the sprite to diplay the framerate
sf::Text frameRate;
frameRate.setPosition(10, 10);

double r = 20; // radius of the circle

What is a vertex array? What do we use that instead of sprites?
vertex array = array of vertices

We use a vertex array to have an efficient way of drawing multiple shape because with using sprite we will loop through the array of bullets and draw each bullet separately. If you have something like hundred of draw calls drawing each sprite separately is not a big deal. But with thousand of draw calls the framerate will drop drastically and the game will be unplayable.

The solution is to put every sprite in one vertex array and with one draw call we can draw the vertex array. The only condition to do that is to use only one texture per vertex array (that's why we have every bullet on one picture).

If you want to learn more about how the vertex array work you can read the official tutorial.

Event loop

Now it's time to create an event loop inside the main loop

sf::Clock clock;
while (window.isOpen()) {
	sf::Event event;
	sf::Time timeElapsed = clock.restart();
	while (window.pollEvent(event)) {
		if (event.type == sf::Event::Closed)

		if (event.type == sf::Event::KeyPressed)
			if(event.key.code == sf::Keyboard::Escape)

As you can see the only event we catch is the closing event, the player is not moved with events but by checking the state of the state of the keyboard directly.
If we use events to handle the movement, we will obtain a jerky movement because there is a small delay to catch events.
// Move the player
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) {
	player.setPosition(player.getPosition().x - velPlayer*timeElapsed.asMilliseconds(), 
else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) {
	player.setPosition(player.getPosition().x + velPlayer*timeElapsed.asMilliseconds(), 

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) {
	player.setPosition(player.getPosition().x, player.getPosition().y 
	- velPlayer*timeElapsed.asMilliseconds());
else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) {
	player.setPosition(player.getPosition().x, player.getPosition().y 
	+ velPlayer*timeElapsed.asMilliseconds());
I use else if only for the opposite direction, not for the adjacent direction, this allows to make diagonals movements by holding two keys.

Updating the bullets

Each frame we need to update the position of the bullet already created, we simply loop through the whole array and check each bullet.

// Update bullets
for (size_t i = 0; i < totalBullets; i++) {
	if (bullets[i] != NULL) {
		sf::Vertex* quad = &vertices[i * 4];
		sf::Sprite *spr = bullets[i]->getSprite();

		quad[0].position = sf::Vector2f(spr->getPosition().x, spr->getPosition().y);
		quad[1].position = sf::Vector2f(spr->getPosition().x + spr->getTextureRect().width, 
		quad[2].position = sf::Vector2f(spr->getPosition().x + spr->getTextureRect().width, 
		spr->getPosition().y + spr->getTextureRect().height);
		quad[3].position = sf::Vector2f(spr->getPosition().x, spr->getPosition().y + 

Add bullet in the array

I will use a helper function to add one bullet in the vertex array.

void addBullet(sf::VertexArray* v, Bullet *bul, size_t index) {

	// Get the quad contained in the vertex array
	sf::Vertex* quad = &(*v)[index*4];

	sf::Sprite *spr = bul->getSprite();

	// Set the position of the sprite
	quad[0].position = sf::Vector2f(spr->getPosition().x, spr->getPosition().y);
	quad[1].position = sf::Vector2f(spr->getPosition().x + spr->getTextureRect().width, 
	quad[2].position = sf::Vector2f(spr->getPosition().x + spr->getTextureRect().width, 
	spr->getPosition().y + spr->getTextureRect().height);
	quad[3].position = sf::Vector2f(spr->getPosition().x, spr->getPosition().y + 

	// Set the texture of the sprite
	quad[0].texCoords = sf::Vector2f(spr->getTextureRect().left, spr->getTextureRect().top);
	quad[1].texCoords = sf::Vector2f(spr->getTextureRect().left + spr->getTextureRect().width, 
	quad[2].texCoords = sf::Vector2f(spr->getTextureRect().left + spr->getTextureRect().width, 
	spr->getTextureRect().top + spr->getTextureRect().height);
	quad[3].texCoords = sf::Vector2f(spr->getTextureRect().left, spr->getTextureRect().top + 

Red bullet pattern

First we do a spiral pattern of medium red bullets.

// Add new red bullet
if (counterTime > bulletTime && counterWave < 1) {
	if (numBullet == maxBullets) {
		numBullet = 0;

	if (bullets[numBullet] != NULL) {
		bullets[numBullet] = NULL;

	float x = r*cos((size_t) (numBullet%180) / Pi);
	float y = r*sin((size_t) (numBullet%180) / Pi);
	Bullet *bul = new Bullet(&tex, sf::Vector2f(x*0.01, y*0.01), sf::Vector2f(winW/2 - 8, winH/2 - 8), 
	sf::IntRect(0, 64, 16, 16));
	bullets[numBullet] = bul;
	addBullet(&vertices, bul, numBullet);
	counterTime = counterTime - bulletTime;

Each time counterTime ms has passed the code will create a red bullet on a circle of radius r. To find the coordinate of the point we use the polar coordinate:

Blue bullet pattern

This time we will use one of the big bullet:

// Add new blue bullet
if (counterTime2 > bulletTime2 && counterWave2 < 3) {
	for (size_t i = 0; i < 45; i++) {
		float y = r*cos((size_t) i / (Pi/2));
		float x = r*sin((size_t) i / (Pi/2));
		Bullet *bul = new Bullet(&tex, sf::Vector2f(x*0.01, y*0.01), 
		sf::Vector2f(winW / 2 - 32, winH / 2 - 32), sf::IntRect(128, 0, 64, 64));
		if (numBullet2 == maxBullets) {
			numBullet2 = 0;

		if (bullets[maxBullets + numBullet2] != NULL) {
			delete(bullets[maxBullets + numBullet2]);
			bullets[maxBullets + numBullet2] = NULL;

		bullets[maxBullets + numBullet2] = bul;
		addBullet(&vertices, bul, maxBullets + numBullet2);
		counterTime2 = counterTime2 - bulletTime2;

Each counterTime2 ms we create 45 bullets around a circle of radius r.

Purple bullet pattern

Time to get crazy for this last pattern ;)

if (counterTime3 > bulletTime3 && counterWave >= 1) {
	for (size_t i = 0; i < 10; i++) {
		float y = r*cos((size_t)rand()%180 / (Pi));
		float x = r*sin((size_t)rand()%180 / (Pi));
		Bullet *bul = new Bullet(&tex, sf::Vector2f(x*0.1, y*0.1), 
		sf::Vector2f(winW / 2 - 4, winH / 2 - 4), sf::IntRect(0, 80, 8, 8));
		if (numBullet3 == 14*maxBullets) {
			numBullet3 = 0;

		if (bullets[2 * maxBullets + numBullet3] != NULL) {
			delete(bullets[2 * maxBullets + numBullet3]);
			bullets[2 * maxBullets + numBullet3] = NULL;
		bullets[2*maxBullets + numBullet3] = bul;
		addBullet(&vertices, bul, 2*maxBullets + numBullet3);
		counterTime3 = counterTime3 - bulletTime3;

Cleaning the mess (Optional)

for (size_t i = 0; i < totalBullets; i++) {
	if (bullets[i] != NULL) {
		if (bullets[i]->getSprite()->getPosition().x > winW 
|| bullets[i]->getSprite()->getPosition().y > winH 
|| (bullets[i]->getSprite()->getPosition().y + bullets[i]->getSprite()->getTextureRect().height) < 0 
|| (bullets[i]->getSprite()->getPosition().x + bullets[i]->getSprite()->getTextureRect().width) < 0) {
			bullets[i] = NULL;

If the bullet is outside the screen we can delete it, in this example the bullet will be deleted when the array is full and the vertex array is not updated.
I have added this part to not forget that SFML draws everything even if it's not on the screen.

Draw everything

It's time to draw everything on the screen and update the counters for the patterns.



sf::Transform transform;

sf::RenderStates states;
states.transform = transform;
states.texture = &tex;

window.draw(vertices, states);

frameRate.setString(std::to_string(1.0f / timeElapsed.asSeconds()));

counterTime = counterTime + timeElapsed.asMilliseconds();
counterTime2 = counterTime2 + timeElapsed.asMilliseconds();
counterTime3 = counterTime3 + timeElapsed.asMicroseconds();

After the main loop: delete the remaining bullets

When the user exit the loop we need to delete every bullets in the array:

for (size_t i = 0; i < totalBullets; i++) {
	if (bullets[i] != NULL) {


More things need to be created to have a complete game, for example a life bar, collisions and more classes to structure the code (example: a resource manager).
For the collisions it's not too complicated in this case, we circle to circle collision.
You can check that with this function:

// Return true if two circles collides
bool CC(sf::Vector3f c1, sf::Vector3f c2) {
	return (c2.x - c1.x) * (c2.x - c1.x) + (c1.y - c2.y) * (c1.y - c2.y) < (c1.z + c2.z) * (c1.z + c2.z);
The parameters are too circle c1 and c2 where:
x = x-coordinate of the center of the circle 
y = y-coordinate of the center of the circle
z = radius of the circle

If you want to have rectangle collisions you can use this functions:
// Return true if two rectangles collides
bool AABB(sf::FloatRect b1, sf::FloatRect b2) {
	return (b2.left < b1.left + b1.width) && (b2.left + b2.width > b1.left) && 
	(b2.top < b1.top + b1.height) && (b2.top + b2.height > b1.top);

You can find a lot of articles about 2d collisions, for example: 2d collision detection

Download the project: BulletHell.zip (VS2015)

Too much white, I can't see blue