Hi shmup-friends,
time for another tutorial. This time I'd like to show you how to build a bullet management system. It will be rather basic, good enough to get you started, efficient enough to not become the bottleneck of your shmup and rather easy to extend.
I did not test any of this code in real life, I wrote all this from scratch in a text editor, so I hope there aren't too many mistakes.
What do we need?First of all let's sum up what essential features are really necessary:
- bullet descriptor.
This will be a very tiny class holding the essential (!) bullet info.
- bullet container.
Somehow all our thousands of bullets must be stored somewhere.
And we need some ways to
- create bullets
- destroy bullets
- look up bullets
- update (move) all bullets
- render all bullets
- check collisions
Let's take a look at those basic building blocks.
Bullet descriptorWhen designing that one you have to consider quite a lot of possibilities. Especially when it comes to different bullet types and different bullet behaviour.
One simple way would be to create a base bullet class, derive different bullet-types from that one and hard-wire the various behaviours and appearances you need:
class bullet_base
|
+--------------------+--------------------+
| |
class class
bullet_straight_line bullet_seek_player
This one may look promising, but in reality your different bullet patterns will be that complex, that you'd end up implementing tons of specialized classed. It's okay for very simple shmups but besides that I wouldn't recommend it.
At some point during your shmup-development you'll most likely want to integrate some kind of scripting, for many reasons, two of them being "quick modification of patterns without recompilation" and "tired of rewriting all your code because you missed a feature you could have had by implementing scripting in the first place".
That's why I recommend:
let's build one single bullet structure that allows for practically anything without blowing up the class or class hierarchy.
class Bullet {
public:
vec2 position;
vec2 direction;
float collision_size;
unsigned int unique_id;
integer active;
Appearance *appearance;
Behaviour *behaviour;
public:
inline void Render(void) {
appearance->PrepareRenderingOf(this);
}
inline void Update(void) {
if(!behaviour->Update(this)) active=0; // returns false if bullet shall be removed
}
};
That's it. Okay, you could add some reference to the enemy that spawned the bullet or similar atomic info, but let's try it with that most compact version first.
The idea is to "outsource" potentially complex functionality to other seperated modules. This has many benefits, probably the most obvious being the fact, that we can build arrays of bullets regardless of their logical type, because they are of the same "compiler"-type and of the same internal byte-size. And we could easily change the behaviour of a bullet on-the-fly by just changing the "behaviour"-pointer.
As you can see the Render and Update methods just call the matching methods of the attached Appearance and Behaviour classes, instead of implementing all that inside the bullet itself.
Bullet containersNow that one is easy. As being said: one benefit of the single-class-bullet-design is that we can stuff all of them in simple arrays.
You could argue that you can do that with derived classes too - but there's a big difference: you could build arrays of POINTERS to your bullets only. But we can use arrays of OBJECTS regardless of their logical type. And that means that we are allowed to build very cache-friendly structures.
Since this is heavily connected with creation and destruction of bullets, we will discuss that more deeply in the next section.
Create, destroy and update bulletsThanks to our simple bullet class design the creation of different bullets could become as trivial as that:
Bullet *l_bullet=new Bullet(position,direction,appearance,behaviour);
As opposed to a different new ClassX for every bullet type.
Well, at least in theory it's that simple. In practice it's a bit different since you don't want to use heap-allocation and -deallocation every time a bullet is created or destroyed.
Instead we'll design a bullet-manager that pre-allocates some thousands of bullets (as much as we possibly need), holds the active bullets and exposes some convinient functions for creation and deletion.
Note that the following code does some maybe unusual things, so better skip it, continue reading below and come back later.
const unsigned int MAX_BULLETS=2000; // inside a common include, at global scope, used at different places
class BulletManager {
private:
Bullet *bullets;
unsigned int *id_index_table; // basically this will be both, a mapping table and a linked list of IDs
unsigned int active_bullets_count;
unsigned int next_free_id_slot;
public:
BulletMananger(void) :
bullets(0),
id_index_table(0),
active_bullets_count(0),
next_free_id_slot(0)
{
bullets=new Bullet[MAX_BULLETS];
id_to_index_table=new unsigned int[MAX_BULLETS];
// initialize the ID-index-table to form a linked list, each entry pointing to the next
for(unsigned int t=0;t<MAX_BULLETS;++t) id_index_table[t]=t+1;
}
~BulletMananger(void) {
delete[] id_to_index_table;
delete[] bullets;
}
unsigned int Create(Bullet &p_bullet_description) {
Bullet &l_new_bullet=bullets[active_bullets_count];
l_new_bullet=p_bullet_description;
l_new_bullet.active=1;
unsigned int l_id=next_free_id_slot;
l_new_bullet.id=l_id;
// remember: next_free_id_slot contains the index of the next free ID, see initialization
// now we store that index to the free ID list for the next bullet that's going to be created later...
next_free_id_slot=id_index_table[l_id];
// ... and instead let the table now point to our bullet's array index!
id_index_table[l_id]=active_bullets_count++;
return l_id;
}
void Update(void) {
unsigned int l_new_index=0;
for(unsigned int t=0;t<active_bullets_count;++t) {
Bullet &l_bullet=bullets[t];
l_bullet.Update();
if(l_bullet.active) {
if(t!=l_new_index) {
// if a bullet was destroyed before adjust the index-tables
// and move the bullet down in the array accordingly.
bullets[l_new_index]=l_bullet;
id_index_table[l_bullet.id]=l_new_index;
}
++l_new_index;
} else {
// when a bullet is removed we have to "fix" the free-IDs-pseudo-linked-list.
// that's easy: we just let the bullet's slot point to the known next free ID
// which we stored during bullet creation...
id_index_table[l_bullet.id]=next_free_id_slot;
// ... and make that slot the new "next_free" slot (like adjusting the head of a linked list)
next_free_id_slot=l_bullet.id;
}
}
active_bullets_count=l_new_index;
}
inline Bullet& GetBulletByID(unsigned int p_id) {
return bullets[id_index_table[p_id]];
}
};
You simply create a bullet by filling the generic Bullet structure and calling BulletManager::Create. As a result you'll get a unique bullet-ID, at least unique for the life-time of that bullet.
It is very important to note, that you don't get a direct pointer to the bullet's internals! The main reason not to use pointers is due to the fact that we internally use an array of Bullet objects, not Bullet pointers. This is primarily done to keep the bullet-structure cache-friendly and to avoid the risk of dead pointers floating around in the system.
But how do we ensure unique IDs together with deletion and how do we implement efficient look-up of "real" objects by that ID?
That magic is done by the code associated with that obscure "id_index_table" and "next_free_id_slot". You have to get used to the idea that a linked-list doesn't have to be implemented via pointers. In fact: here we essentially got some kind of linked-list that operates on indices. And the indices have two uses: either they point to the next free ID or they point to the concrete bullet object.
Maybe it's easier to understand if I tell you the order in which IDs are generated:
bullet A created: 0
bullet B created: 1
bullet C created: 2
bullet B destroyed, its ID 1 is becoming the first element in the free ID list
bullet D created: 1
bullet E created: 3
Anyway, now looking up a bullet is as easy as calling "GetBulletByID".
Usually you'd use that to reference bullets inside of more complex scripts.
Let's sum up the main benefits of our approach here:
1. the bullets are inside one tight array, regardless of logical type.
2. there are no "gaps" inside the array. During the update-loop dead bullets are automatically sorted out.
3. the IDs remain valid even if the objects are moved around internally to preserve a tight array.
4. the drawing order of the bullets is not changed.
5. it's just a few lines of code

Note:
Usually you just want one such manager-object inside your shmup, but you're allowed to use multiple instances. If you want more than one you must be aware that the IDs created by the manager are only unique as long as only one instance is used. You can overcome this limit by adding/subtracting a per-manager magic-number (multiples of MAX_BULLETS) when using the IDs.
Update bullets in detailIn the above code we already saw the update-loop. But the concrete update-behaviour was still missing. To implement that we have to define a "Behaviour" class which we can assign to bullets:
class Behaviour {
public:
Behaviour(void);
virtual ~Behaviour(void); // don't forget the virtual keyword
virtual bool Update(Bullet *p_bullet)=0; // returns false to destroy a bullet
};
As you can see this is a virtual class which just defines a common interface for real behaviours. It's rather slim because for the bullet-management only one thing counts: that we can instruct our behaviour to update one dedicated bullet.
Let's implement a simple working behaviour now:
class Behaviour_Straight : public Behaviour {
public:
Behaviour_Straight(void) : Behaviour() {}
virtual bool Update(Bullet *p_bullet) {
p_bullet.position+=p_bullet.direction*5.0f; // move bullet 5 units in its original direction
return InsideScreen(p_bullet.position);
}
};
The inner working of this class should be self-explaining.
To get a bullet to use that behaviour you just set its Behaviour-pointer-attribute to point to a Behaviour_Straight-object. Important: one single Behaviour-object can be shared by an arbitrary number of bullets!
As already mentioned, in reality you'll most likely use some scripting system for your bullets, so chances are that you need a "Behaviour_Script" class that's initialized with the necessary script. There appears to be one problem though:
The bullets don't carry around any additional information you may need for your scripts, like some status variables or whatever. Luckily we already have the solution at hand: the bullet ID the bullet-manager takes care of.
The bullet ID is unique for the life-time of a bullet and is in the range 0-MAX_BULLETS. So you can add whatever meta-bullet-info your behaviour may need to store with a bullet by creating an array of info inside the behaviour class:
class Behaviour_Script : public Behaviour {
private:
ScriptBulletContext *bullet_meta_data;
Script *script;
public:
Behaviour_Script(void) : Behaviour() {
script=new Script("scripfile");
bullet_meta_data=new ScriptBulletContext[MAX_BULLETS];
};
virtual ~Behaviour_Script(void) {
delete[] bullet_meta_data;
delete script;
}
virtual bool Update(Bullet *p_bullet) {
if(p_bullet->active==1) {p_bullet->active=2; bullet_meta_data[p_bullet->ID].Reset();}
return script->Run(p_bullet,bullet_meta_data[p_bullet->ID]);
}
};
This is all pseudo-code but I hope it shows how it might work. The line
if(p_bullet->active==1) {p_bullet->active=2; bullet_meta_data[p_bullet->ID].Reset();}
just resets the meta-data of a bullet if that bullet was just spawned. That works because the bullet-manager sets Bullet.active to 1 if a new one is created and it only cares wether active becomes 0. Any other value is ignored and can be used for things like that without harming the manager.
In reality you may want to add more methods to the Behaviour base class, like
class Behaviour {
...
public:
virtual void CreateBullet(Bullet *p_bullet)=0;
virtual void DestroyBullet(Bullet *p_bullet)=0;
};
That way you could implement such (de)initialization functionality more easily. Or other funny stuff like a cluster bomb that's activated on bullet destruction. For this tutorial I tried to keep everything as compact as possible, since it's already becoming rather complex.
Render bulletsThis works in basically the same way as the update-system with its Behaviour classes.
Since we want different looking bullets we'll design our "Appearance" class using the classic base/derived class design. The base class is essentially only the common interface that allows us to connect it to a bullet. It's therefore a virtual base-class that doesn't do anything by itself:
class Appearance {
public:
Appearance(void);
virtual ~Appearance(void); // don't forget the virtual keyword
virtual void PrepareRenderingOf(Bullet *p_bullet)=0;
virtual void RenderAllBullets(void)=0;
};
As we all know graphics hardware likes big batches. Therefore it's essential that we group our bullets by their appearance, built a large block of vertices out of those and throw that block at the GPU.
The cool thing is: with our design we can get perfectly ordered arrays of bullet-drawing-info for bullets of the same type (almost) for free.
As you can see the above interface is already prepared for that, the methods "RenderAllBullets" tells it all. Now let's implement a possible "real" appearance class:
class Appearance_PointSprite : public Appearance {
struct BulletRenderInfo {
vec2 position;
vec2 direction;
inline void Set(const Bullet *p_bullet) {
position=p_bullet->position;
direction=p_bullet->direction;
}
};
private:
BulletRenderInfo *render_info;
unsigned int render_info_count;
GLuint texture_id;
Shader *shader; // I won't get into this
public:
Appearance(GLuint p_texture_id) :
render_info(0),
render_info_count(0),
texture_id(p_texture_id),
shader(0)
{
render_info=new BulletRenderInfo[MAX_BULLETS]; // remember, our global constant
shader=new PointSpriteShader(); // just to show HOW it may work, won't go into details
}
virtual ~Appearance(void) {
delete[] render_info;
}
virtual void PrepareRenderingOf(Bullet *p_bullet) {
render_info[render_info_count].Set(p_bullet->position);
++render_info_count;
}
virtual void RenderAllBullets(void) {
if(render_info_count) {
::glBindTexture(texture_id);
::glEnable(GL_BLEND);
::glColor4ub(255,255,255,255);
::glDisable(GL_DEPTH_TEST);
::glDepthMask(GL_FALSE);
::glEnable(GL_POINT_SPRITE_ARB);
::glPointSize(15.0f);
::glVertexPointer(4,GL_FLOAT,sizeof(BulletRenderInfo),render_info);
shader->Activate();
::glDrawArrays(GL_POINTS,0,render_info_count)
shader->Deactivate();
render_info_count=0; // Important! Reset counter for next frame.
}
}
};
Fine, but how does the appearance class know which bullets to draw? It never references any of our bullets. Answer: As with the Behaviour-class it's the other way around, each bullet knows its appearance.
That brings us back to our bullet-class:
class Bullet {
....
inline void Render(void) {
appearance->PrepareRenderingOf(this);
}
};
I guess you already know how the rendering loop looks like. We will now extend out BulletManager class by an appropriate method:
class BulletManager {
...
public:
void Render(void) {
for(unsigned int t=0;t<active_bullets_count;++t) {
bullets[t].Render();
}
}
};
After calling BulletManager::Render all active bullets have instructed their attached Appearance-classes to prepare all render-buffers. All that needs to be done is to loop over all those Appearance-objects and to tell them to actually draw themselfs.
Collision checksFor 99% of all bullet types you can get away with either a bullet-bounding-rectangle or a bullet-bounding-sphere collision vs. player-position-point check. For the remaining 1% you could add special check behaviour to your "Behaviour" class.
For the standard checks we just need to add another tiny function to our bullet-manager. Our bullets already carry a "collision_size" attribute and a position, that's all we need:
class BulletManager {
....
public:
unsigned int CheckBulletsAgainstPlayerPosition(const vec2 &p_player_position)
{
unsigned int l_hit_count=0;
for(unsigned int t=0;t<active_bullets_count;++t) {
const Bullet &l_bullet=bullets[t];
vec2 l_dif=bullet.position-p_player_position;
if(l_dif.Length()<bullet.collision_size) {
++l_hit_count;
break; // uncomment this if you want to allow multiple hits on the player
}
}
return l_hit_count;
};
};
This is one of the most simple approaches. It'll work but there are far more advanced ways to handle collisions. But that's stuff for another tutorial motorherp already covered
Putting it all togetherPost-size limit reached

Daniel