in need of some opinions
The Master
Hiya. I'm after a second opinion on this particle engine that I've done. It is part of a much larger interface so some of the coding might appear foreign. Especially the Sprite class. Pay no attention to this as the Sprite class has already been debugged, refined, and tested hundreds of times. The particle engine is simple. I've commented the code here so, please have a look and any advice would be appreciated.

header file:
[code]
#ifndef PARTICLE_H
#define PARTICLE_H


#include <allegro.h>
#include <math.h>
#include "Sprites.h"




#define PARTICLE_OK AL_ID('P','C','O','K')
#define PARTICLE_MEMORY AL_ID('P','M','E','M')
#define PARTICLE_TIMER AL_ID('P','T','I','M')



// particle animation types
#define EXPLOSION AL_ID('A','E','X','P') // the particles are forced outward
#define IMPLOSION AL_ID('A','I','M','P') // the particles are drawn inward

// particle primitive types
#define PRIM_LINE AL_ID('L','I','N','E') // line
#define PRIM_SPRITE AL_ID('S','P','R','I') // animation
#define PRIM_CIRCLE AL_ID('C','I','R','C') // circle


struct point_t {
int x, y;
};




struct Particle {
point_t position;

volatile int time_of_emission;
volatile int current_time;

float speed;

float size;
float bearing;
float distance;

volatile int lifetime;
};




static volatile int particle_counter = 0;
void ParticleTick(void);




class ParticleAnimation {
public:
ParticleAnimation();
~ParticleAnimation();

private:
Particle *ParticleList;
int nParticles;

int c1, c2; // color 1, color 2
BITMAP *texture; // texture (if the effect requires it)
Sprite *spr; // sprite (if the effect requires it)
float falloff; // fall-off factor for the particles (just a constant float)

int duration; // may be switched off at anytime if equal to -1
volatile int start_time; // particle_counter value at the start of this animation

int anim_type; // Either EXPLOSION or IMPLOSION
int prim_type; // either LINE, SPRITE, or CIRCLE

point_t position; // position on the screen
point_t centre; // centre on the output bitmap
float maxSize; // max size of the particle
float maxDistance; // max distance of the particle from the centre

BITMAP *pOut; // output bitmap
bool used; // whether or not this memory space is in use

public:
int CreateParticleAnimation(int maxp, int col1, int col2, BITMAP *t, Sprite *s, float f, int d, int at, int pt, int x, int y, float mSize, float mDis);

/*
With particles there are animation types and primitive types:
Animation types:
Explosion - the particles move away from the source at a decreasing speed
Implosion - the particles move towards the source at an increasing speed
Primitive types:
Line - Just a simple line where position is the midpoint of the line. This uses
texture for effect, but uses an interpolation between c1 and c2 if texture
is equal to null. Length of the line is proportionate to speed, time, and
inversely proportionate to maxSize
Sprite - Animates a sprite. The sprite is drawn, taking x and y origin values into
account, at position, and uses an interpolation of c1 and c2 for colorization.
Size is a relative scale factor between 0.0f and 1.0f.
Circle - Draws a circle with centre position. Like Line, size is proportionate to speed,
time, and inversely proportionate to maxSize. Texture is used for effect, or
the c1 value is interpolated from the outside inwards to c2 if texture equals
null.

Note: texture and sprite cannot both be used.
*/

// respective primitive type processors
int ProcessLine(int pIn);
int ProcessSprite(int pIn);
int ProcessCircle(int pIn);

// respective animation type processors
int ProcessExplosion(int pIn);
int ProcessImplosion(int pIn);

// process the animation
int ProcessAnimation();

// render from output bitmap to dest
void Render(BITMAP *dest);

// the screen position
point_t *GetScreenPosition();
void SetScreenPosition(point_t *p);

// free the memory
void DestroyParticleAnimation();

// new particles
int AddParticle(float bearing, int eType);
int RemoveParticle(int pIn);

// the memory space used?
bool InUse() { return used; }
void IsUsed() { used = true; }
void UnUsed() { used = false; }
};





class ParticleEngine {
public:
ParticleEngine();
~ParticleEngine();

private:
ParticleAnimation *pAnim;
int nP; // number of animations

public:
// initialize the engine and allocate space
int CreateParticleEngine(int nParticleAnimations);
// Process the engine
int ProcessParticleEngine();
// free the memory used by the engine
void DestroyParticleEngine();

int Get_nP() { return nP; }

// new animations
int AddParticleAnimation(ParticleAnimation *pA);
void RemoveAnimation(int index);
};






#endif
[/code]





source file:
[code]
#include "Particle.h"




void ParticleTick(void) {
particle_counter++;
}

END_OF_FUNCTION(ParticleTick)





ParticleAnimation::ParticleAnimation() {
ParticleList = NULL;
nParticles = 0;

c1 = c2 = 0;
texture = NULL;
spr = NULL;

falloff = 0.0f;

duration = -1;
start_time = -1;

anim_type = -1;
prim_type = -1;

maxSize = -1;
maxDistance = -1;

pOut = NULL;

used = false;
}




ParticleAnimation::~ParticleAnimation() {
if(spr)
spr->DeleteSprite();

if(texture)
destroy_bitmap(texture);

if(pOut)
destroy_bitmap(pOut);
}





int ParticleAnimation::CreateParticleAnimation(int maxp, int col1, int col2, BITMAP *t, Sprite *s, float f, int d, int at, int pt, int x, int y, float mSize, float mDis) {
nParticles = maxp;
ParticleList = (Particle*)malloc(sizeof(Particle)*nParticles); // allocate memory
if(!ParticleList)
return PARTICLE_MEMORY;

c1 = col1;
c2 = col2;

texture = t;
spr = s;

if(at == EXPLOSION)
falloff = f; // the particles move away from the origin
else if(at == IMPLOSION)
falloff = -f; // the particles move towards the origin

duration = d;
start_time = particle_counter;

anim_type = at;
prim_type = pt;

maxSize = mSize;
maxDistance = mDis;

position.x = x;
position.y = y;

int width = 2 * ( maxDistance + maxSize );

pOut = create_bitmap(width, width); // allocate memory for the output
if(!pOut) {
free(ParticleList);
return PARTICLE_MEMORY;
}

centre.x = width / 2;
centre.y = width / 2;

clear_bitmap(pOut);

return PARTICLE_OK;
}





// called for a line-type particle
int ParticleAnimation::ProcessLine(int pIn) {
point_t p1, p2;

// the position of the particle is midpoint of the line. therefore:

// Miscellaneous #1
p1.x = ParticleList[pIn].position.x - ( ( ParticleList[pIn].size * cosf( ParticleList[pIn].bearing ) ) / 2 );
p1.y = ParticleList[pIn].position.y - ( ( ParticleList[pIn].size * sinf( ParticleList[pIn].bearing ) ) / 2 );

p2.x = ParticleList[pIn].position.x + ( ( ParticleList[pIn].size * cosf( ParticleList[pIn].bearing ) ) / 2 );
p2.y = ParticleList[pIn].position.y + ( ( ParticleList[pIn].size * sinf( ParticleList[pIn].bearing ) ) / 2 );

// set the drawing mode
if(texture != NULL) {
drawing_mode(DRAW_MODE_COPY_PATTERN, texture, 0, 0);
} else {
drawing_mode(DRAW_MODE_SOLID, NULL, 0, 0);
}

int b = p2.y - p1.y;
int x, y, c, d;

for(y = p1.y; y != p2.y+b; y++) {
x = ( ( ( p2.x - p1.x ) * ( y - p1.y ) ) / ( p2.y - p1.y ) ) + p1.x; // two-point formula for a line (miscellaneous #3)
d = sqrtf( ( x - p1.x ) * ( x - p1.x ) + ( y - p1.y ) * ( y - p1.y ) ); // get the distance of current point from start
c = c1 + ( ( c2 - c1 ) * ( ( ftofix(ParticleList[pIn].size) - d ) / ftofix(ParticleList[pIn].size) ) ); // interpolate color based on distance of current point to end (miscellaneous #2)

putpixel( pOut, x, y, c );
}

// reset the drawing mode
drawing_mode(DRAW_MODE_SOLID, NULL, 0, 0);

return 0;
}






int ParticleAnimation::ProcessSprite(int pIn) {
int x = ParticleList[pIn].position.x;
int y = ParticleList[pIn].position.y;

// don't worry about this one

spr->UpdateAnimation();

spr->Render(pOut, x, y, true, 0);
return 0;
}






int ParticleAnimation::ProcessCircle(int pIn) {
if(texture != NULL) {
drawing_mode(DRAW_MODE_COPY_PATTERN, texture, 0, 0);
} else {
drawing_mode(DRAW_MODE_SOLID, NULL, 0, 0);
}

int c;
int x = ParticleList[pIn].position.x;
int y = ParticleList[pIn].position.y;

for(int r = ParticleList[pIn].size; r > 0; r--) {
// miscellaneous #2
c = c1 + ( ( c2 - c1 ) * ( ( ftofix(ParticleList[pIn].size) - r ) / ftofix(ParticleList[pIn].size) ) );
circlefill(pOut, x, y, r, c);
};

drawing_mode(DRAW_MODE_SOLID, NULL, 0, 0);

return 0;
}





int ParticleAnimation::ProcessExplosion(int pIn) {
int x, y;
float bearing;

bearing = ParticleList[pIn].bearing;

x = ParticleList[pIn].position.x + ftofix( ParticleList[pIn].distance * cosf( bearing ) );
y = ParticleList[pIn].position.y + ftofix( ParticleList[pIn].distance * sinf( bearing ) );

ParticleList[pIn].position.x = x;
ParticleList[pIn].position.y = y;

return 0;
}





int ParticleAnimation::ProcessImplosion(int pIn) {
int x, y;
float bearing;

bearing = ParticleList[pIn].bearing;

x = ParticleList[pIn].position.x - ftofix( ParticleList[pIn].speed * cosf( bearing ) );
y = ParticleList[pIn].position.y - ftofix( ParticleList[pIn].speed * sinf( bearing ) );

ParticleList[pIn].position.x = x;
ParticleList[pIn].position.y = y;

return 0;
}






int ParticleAnimation::ProcessAnimation() {
volatile int dietime = start_time + duration;

if(dietime == particle_counter && duration != -1) {
DestroyParticleAnimation();
return 0;
}

for(int pIn = 0; pIn < nParticles; pIn++) {
if(ParticleList[pIn].time_of_emission != -1) {
dietime = ParticleList[pIn].lifetime;

ParticleList[pIn].current_time = particle_counter - ParticleList[pIn].time_of_emission;
if(ParticleList[pIn].current_time > dietime)
return RemoveParticle(pIn);

switch(anim_type) {
case EXPLOSION:
// EXP#1
ParticleList[pIn].speed = falloff * sqrt((float)(ParticleList[pIn].current_time * ParticleList[pIn].speed));
// EXP#2
ParticleList[pIn].distance += ParticleList[pIn].speed;
// EXP#3
ParticleList[pIn].size = ( falloff * ParticleList[pIn].distance ) / maxSize;

ProcessExplosion(pIn);
break;
case IMPLOSION:
// IMP#1
ParticleList[pIn].speed = ( maxDistance * falloff ) / ( sqrt( 1 + ParticleList[pIn].distance ) );
// IMP#2
ParticleList[pIn].distance += ParticleList[pIn].speed;
// IMP#3
ParticleList[pIn].size = ( ParticleList[pIn].distance / maxDistance ) * maxSize;

ProcessImplosion(pIn);
break;
};

switch(prim_type) {
case PRIM_LINE:
ProcessLine(pIn);
break;
case PRIM_SPRITE:
ProcessSprite(pIn);
break;
case PRIM_CIRCLE:
ProcessCircle(pIn);
break;
};

srand(particle_counter);
int angle = rand() % 360;

AddParticle((float)((angle*AL_PI)/180), anim_type);
}
};

return 0;
}




void ParticleAnimation::Render(BITMAP *dest) {
masked_blit(pOut, dest, 0, 0, position.x, position.y, pOut->w, pOut->h);
}






point_t *ParticleAnimation::GetScreenPosition() {
return &position;
}







void ParticleAnimation::SetScreenPosition(point_t *p) {
position.x = p->x;
position.y = p->y;
}






void ParticleAnimation::DestroyParticleAnimation() {
if(ParticleList)
free(ParticleList);

if(pOut)
destroy_bitmap(pOut);

UnUsed();
}





int ParticleAnimation::AddParticle(float bearing, int eType) {
for(int i=0; ParticleList[i].time_of_emission != -1; i++);

if(i >= nParticles)
return -1;

ParticleList[i].bearing = bearing;
ParticleList[i].time_of_emission = particle_counter;
ParticleList[i].current_time = 0;

switch(eType) {
case EXPLOSION:
ParticleList[i].distance = 0.0f;
ParticleList[i].size = 0.0f;
ParticleList[i].speed = falloff * sqrt( 1.0f );

// EXP#4
ParticleList[i].lifetime = ( maxSize*maxSize ) / ( falloff * ParticleList[i].speed );
break;
case IMPLOSION:
ParticleList[i].distance = maxDistance;
ParticleList[i].size = maxSize;
ParticleList[i].speed = ( falloff * maxDistance ) / sqrt( 1.0f + ParticleList[i].distance );

// IMP#4
ParticleList[i].lifetime = maxDistance / ( ParticleList[i].speed * maxSize * maxSize );
break;
}

int quadrant;
float t = fabs( (AL_PI/2) - bearing ); // convert to true bearing

if(t >= 0 && t <= AL_PI/2)
quadrant = 1;
else if(t > AL_PI/2 && t <= AL_PI)
quadrant = 2;
else if(t > AL_PI && t <= (3*AL_PI)/2)
quadrant = 3;
else if(t > (3*AL_PI)/2 && t <= 2*AL_PI)
quadrant = 4;

int x, y;

x = ParticleList[i].distance * cosf( bearing );
y = ParticleList[i].distance * sinf( bearing );

switch(quadrant) {
case 1:
ParticleList[i].position.x = x;
ParticleList[i].position.y = y;
break;
case 2:
ParticleList[i].position.x = x;
ParticleList[i].position.y = -y;
break;
case 3:
ParticleList[i].position.x = -x;
ParticleList[i].position.y = -y;
break;
case 4:
ParticleList[i].position.x = -x;
ParticleList[i].position.y = y;
break;
};

return i;
}






int ParticleAnimation::RemoveParticle(int pIn) {
ParticleList[pIn].time_of_emission = -1;
return pIn;
}






ParticleEngine::ParticleEngine() {
nP = 0;
}





ParticleEngine::~ParticleEngine() {

}





int ParticleEngine::CreateParticleEngine(int nParticleAnimations) {
nP = nParticleAnimations;
pAnim = (ParticleAnimation*)malloc(sizeof(ParticleAnimation) * nP);
if(!pAnim)
return PARTICLE_MEMORY;

if(install_int_ex(tick, MSEC_TO_TIMER(25)) < 0) {
return PARTICLE_TIMER;
}

return PARTICLE_OK;
}





int ParticleEngine::ProcessParticleEngine() {
for(int a=0; a < nP; a++) {
pAnim[a].ProcessAnimation();
}

return 0;
}





void ParticleEngine::DestroyParticleEngine() {
if(pAnim)
free(pAnim);

nP = 0;
}




int ParticleEngine::AddParticleAnimation(ParticleAnimation *pA) {
for(int a=0; pAnim[a].InUse() == true; a++);

if(a >= nP)
return -1;

pAnim[a] = *pA;
pAnim[a].IsUsed();
return a;
}




void ParticleEngine::RemoveAnimation(int index) {
pAnim[index].DestroyParticleAnimation();
}

[/code]



I've commented references to equations in the attached word document for those who can't understand the calculations.

Just have a look, see what you think. Tell me what should be changed.
Indeterminatus

Alright, here are the things that come to mind when skimming through your sources. Please note that this is not a complete (or even formally correct) code review, so they're just suggestions/ideas you can consider or throw away.

// particle animation types
#define EXPLOSION    AL_ID('A','E','X','P')  // the particles are forced outward
#define IMPLOSION    AL_ID('A','I','M','P')  // the particles are drawn inward

...

int anim_type;    // Either EXPLOSION or IMPLOSION
int prim_type;    // either LINE, SPRITE, or CIRCLE

This looks like a perfect application for an enum. Generally, try to avoid preprocessor #defines where you can.

Also, use consistent nomenclature. I've seen anim_type and maxDistance. Unless you have a very good reason to distinct those two by using different styles, I suggest you choose one and stick to that.

int CreateParticleAnimation(int maxp, int col1, int col2, BITMAP *t, Sprite *s, float f, int d, int at, int pt, int x, int y, float mSize, float mDis);

This has too many parameters for me. Rule of thumb is to limit parameters to 7, the less, the better. There might be need for more complex structures/patterns to circumvent this, so it's probably easier to just keep it as-is.

This one's mostly stylistic: I always prefer the order public, protected, private to foster the idea of an interface. Unfortunately, C++ doesn't allow truely hidden members. There's the Cheshire cat pattern (also known as PImpl), but that's still not "it". Think about what might be of interest to someone just using the class (not extending it). Those things are best put at the top, there's no need to look at private members.

Hmmm ... why CreateParticleAnimation and DestroyParticleAnimation? Sounds like a constructor/destructor pair. Maybe you don't need those methods at all.

  bool InUse() { return used; }
  void IsUsed() { used = true; }
  void UnUsed() { used = false; }

By reading the names of these methods, I would never guess the actual outcome. Try to use better names, especially InUse() and IsUsed() are confusing. On a similar note, you can make InUse() a const method, like so: bool InUse() const { ... }. Rule of thumb is to make as much const as possible.

if(texture)
    destroy_bitmap(texture);

Two things: One, after you destroy_bitmap the texture, set it to 0. Two, and this is a very pedantic style issue, compare texture to 0. Everything not a boolean should be compared to a value. Someone reading if(texture) might deduce that texture in fact is a valid pointer inside its block, but that would be misleading (mind dangling pointers).

The whole type-dependent stuff (line, sprite, circle, and implosion/explosion) looks like a perfect application for polymorphism. You could extract the relevant behaviour to classes of their own, thus encapsulating functionality more. Something to think about ;).

void ParticleAnimation::SetScreenPosition(point_t *p) {
  position.x = p->x;
  position.y = p->y;
}

This one's unsafe. What happens if p is 0? Also, you make a deep copy anyway, so I suggest passing p as const point_t &.
(edit: Even better would be to let point_t handle the copying, and just do position = p; in SetScreenPosition. If you don't have any pointers or sensitive data in point_t, this works out of the box. If you do, you're well advised to implement operator = yourself.)

pAnim = (ParticleAnimation*)malloc(sizeof(ParticleAnimation) * nP);

Hm. Unless you want to the plain pointer approach for educational purposes, I suggest you switch to an STL container, like std::vector or std::list. There are already enough resources dealing with those (also here in the forum), so I won't go into detail here.

Well, that's about it what came to my mind. Further comments would require better analysis of your code's structure, which I cannot afford time-wise right now. Hope you can make some sense of my drivel.

Good luck with your particle engine!

The Master
Quote:

This looks like a perfect application for an enum. Generally, try to avoid preprocessor #defines where you can.

Haven't used enums in ages, but I might look into that. I don't really remember how to use them, but i've got plenty of books and info on the subject.

Quote:

Hmmm ... why CreateParticleAnimation and DestroyParticleAnimation? Sounds like a constructor/destructor pair. Maybe you don't need those methods at all.

In CreateParticleAnimations, there is memory allocated, and if an error occurs the error message is returned. I generally don't like using malloc or any of those kinds of routines in constructors. But perhap I can put the DestroyParticleAnimation in a destructor.

Quote:

Two things: One, after you destroy_bitmap the texture, set it to 0. Two, and this is a very pedantic style issue, compare texture to 0. Everything not a boolean should be compared to a value. Someone reading if(texture) might deduce that texture in fact is a valid pointer inside its block, but that would be misleading (mind dangling pointers).

Alright, so you mean if texture == NULL? Fair enough. Also, I've found that if i tried to destroy a null bitmap it gave a massive runtime error. A lot of my programming habits have come from my experiences with DirectX, hence my use of classes and such.

Quote:

By reading the names of these methods, I would never guess the actual outcome. Try to use better names, especially InUse() and IsUsed() are confusing. On a similar note, you can make InUse() a const method, like so: bool InUse() const { ... }. Rule of thumb is to make as much const as possible.

What purpose does const serve when it comes to functions? Seriously, I don't know. Furthermore, these are for internal use by the ParticleEngine, and not intended to be called by another class or function.

Quote:

Hm. Unless you want to the plain pointer approach for educational purposes, I suggest you switch to an STL container, like std::vector or std::list. There are already enough resources dealing with those (also here in the forum), so I won't go into detail here.

That was a oversight on my part. usually i only use malloc for structures, and the new keyword for classes. but i've not needed an array of classes for a while, so malloc has become second nature. Would a better approach be:

pAnim = new ParticleAnimation[nP];

By the way, did you see the thread i started about how much I hate RLE sprites? I used vectors fo those, and it gave me runtime errors all over the place. And i generally prefer not to go there as they're just too bloody hard to understand and there are no coherant tutorials i can find on the net. I prefer malloc or new.

Thanks for your comments. Just so you know, I am not intending on making this a user-friendly Particle Engine; it is purely another building block in my current programming project. And since I won't be releasing the final source code for the game, I don't see any reason to make the nomenclature neat and clean. Not to sound rude. However, these comments have been found useful in my own analysis.

Indeterminatus
Quote:

What purpose does const serve when it comes to functions? Seriously, I don't know. Furthermore, these are for internal use by the ParticleEngine, and not intended to be called by another class or function.

It makes the this pointer const, meaning you'll get a compile error if you try to modify any non-mutable members. The purpose of this is that a method declared as const guarantees that it doesn't change the object's state. If those methods are intended for internal use only, you should hide them by making them private.

Quote:

And since I won't be releasing the final source code for the game, I don't see any reason to make the nomenclature neat and clean. Not to sound rude.

Not sounding rude, no worries :). After all, it's your project, not mine, so do whatever you see fit. However, for your own sake, I want to stress the point of consistent nomenclature for future work. As a programmer, you shouldn't have to look up whether a specific variable's name was max_size or maxSize (for example). With consistent naming, you'll never run into such situations. Let the code rest for a few days, and it will become an issue if you have to extend/modify it later on.

(edit: The bottom-line being that you shouldn't worry about "low-level" stuff like this. Better break your head over algorithms. )

Thread #587698. Printed from Allegro.cc