How do I handle particles (e.g. bullets)?

Started by
4 comments, last by hyperfine 9 years, 8 months ago

Hi folks,

I've been toying around with writing a 2-D platformer in the style of 16-bit era games (think Super Metroid, Mega Man). I'm using SDL and writing all the code in C++. The project is mainly a hobby I am doing for fun, but I'm also trying to pick up some programming skills along the way. Therefore, I'm writing the "engine" myself (if the primitive thing I've written so far can even be called that).

So far I have only a few basics in the game: a room rendered to the screen that has walls and a floor, a character that can run, jump, and shoot, etc. While programming, I was trying to figure out how I should handle the bullets that the character shoots. So far, I have the character able to create bullets that disappear when they hit a wall. What I'd like to do is change the clipping from my sprite sheet when the bullet hits a wall, to create a bit of an explosion effect. Oddly enough, this seemingly easy thing to do is causing me more trouble than anything I've done so far. I'm not terribly worried about making my code super efficient or elegant, but the solutions I've hacked together are just too ugly to allow in my code and will lead to problems down the line. Also, since I'm not able to come up with a pretty solution on my own it leads me to believe that I'm handling bullets the wrong way in general. This is where I'd like some advice.

What I have so far is a character class describing my character, called "Hero". In my main game loop, I have a function Hero.MoveHero that takes input from the gamepad, and handles movement like running, jumping, etc. The end of that function links to the MoveGun function, which is also part of my Hero class. MoveGun is like MoveHero, but allows for creation of bullets and handles the motion of the bullets that are already present. Following my original thinking, this function would also be responsible for changing the clipping of the bullet when it hits a wall so that it looks like an explosion.

Am I going about this the right way? Should I write a hierarchy of classes for particles, one of which is the main character's bullets? One solution I thought of was to create a bullet class. The Hero.MoveHero function allows for creation of a bullet, which sends a flag to the Bullet class where the bullet is then taken care of. I could then have a vector of these bullet classes describing all of the bullets currently in play, and add/subtract from the vector when necessary.

I'm a very novice programmer, so it's very likely that I'm not explaining things well/my ideas are very poor. I'd love feedback.

Thanks!

Advertisement

How many bullets do you have on-screen at once?

Is it out of the question, performance-wise, to just have an std::vector of a struct called 'Bullet', and iterate over it every frame? It's faster to have a single function call that iterates over the elements. UpdateAllBullets(vector), DrawAllBullets(vector). The bullets themselves would just be structs. If, during the UpdateAllBullets() function, one of the bullets hits a wall, then set a flag to 'exploding' instead, and during drawing, that bullet could use the explosion cliprects instead of the bullet cliprects.

Something like:

struct Bullet
{
     typedef std::vector<Bullet> BulletArray;
     enum State {Flying, Exploding, Dead};
    
     //Static functions.
     void AddNewBullet(position, direction, BulletArray &bullets);
     void UpdateAllBullets(float deltaTime, const BulletArray &bullets);
     void DrawAllBullets(Screen &screen, const BulletArray &bullets);
 
     //Shared between every bullet, since it's the same for every bullet.
     static Texture SpriteSheet;
     static Rect BulletClipRect;
     static std::vector<Rect> ExplosionFrames;
     static float Velocity; //The speed of every bullet, per second.
     static unsigned MaxBullets; //Max bullets at any one time.
     
     //Different for every bullet, because they are in different states.
     PointF position;
     PointF direction;
     int frame = 0; //Used when exploding.
};
 
void Bullet::AddNewBullet(PointF position, PointF direction, BulletArray &bullets)
{
     //Find a dead bullet to re-use.
     for(Bullet &bullet : bullets)
     {
          if(bullet.state == Bullet::Dead)
          {
               bullet.state = Bullet::Flying;
               bullet.position = position;
               bullet.direction = direction;
               bullet.frame = 0;
          }
     }
     
     //If we can't find a dead bullet to re-use, push back a new bullet (only if we haven't reached are maximum).
     //It's actually better to just pre-allocate the vector full of 'MaxBullets' of pre-Dead bullets at startup, and just never grow the vector.
     if(bullets.size() < Bullet::MaxBullets)
     {
          bullets.push_back(Bullet(position, direction));
     }
}
 
void Bullet::UpdateAllBullets(float deltaTime, const BulletArray &bullets, World &world)
{
     static float accumulatedTime += deltaTime;
 
     //Figure out how many explosion frames to skip over.
     int framesToUpdate = (accumulatedTime / DelayBetweenFrames);
     accumulatedTime %= DelayBetweenFrames;
     
     for(Bullet &bullet : bullets)
     {
          if(bullet.state == Bullet::Dead)
          {
               continue; //Ignore dead bullets.
          }
          else if(bullet.state == Bullet::Exploding && framesToUpdate > 0)
          {
               bullet.frame += framesToUpdate;
               if(bullet.frame >= Bullet::ExplosionFrames.size())
               {
                    //Remove the bullet when its explosion animation has ended.
                    bullet.state = Bullet::Dead;
               }
          }
          else
          {
               Point oldPosition = bullet.position;
               Point newPosition = oldPosition + (bullet.direction * deltaTime * Bullet::Velocity);
 
               CollisionEvent collision = world.CheckForCollisionBetween(oldPosition, newPosition);
 
               if(collision.collisionOccured)
               {
                    bullet.position = collision.collisionPosition;
                    
                    if(collision.wasEntity)
                    {
                          Entity &entity = world.GetEntityFromID(collision.entityID);
                          entity.GetHitByBullet();
                          
                          //Skip explosion.
                          bullet.state = Bullet::Dead;
                    }
                    else //If we hit a wall.
                    {
                          bullet.state = Bullet::Exploding;
                          bullet.frame = 0;
                    }
               }
               else //No collision.
               {
                     bullet.position = newPosition;
               }
          }
     }
}
 
void Bullet::DrawAllBullets(Screen &screen, const BulletArray &bullets)
{
    for(const Bullet &bullet : bullets)
    {
         const Rect &clipRect = (bullet.state == Exploding? Bullet::ExplosionFrames.at(bullet.frame) : Bullet::BulletClipRect);
         DrawImage(screen, bullet.position, Bullet::SpriteSheet, clipRect);
    }
}

@Servant: Really concrete answer smile.png

Duríng skimming through the code, I saw a small conceptional flaw here:


         else if(bullet.state == Bullet::Exploding && framesToUpdate > 0)
         {
               bullet.frame += framesToUpdate;
               if(bullet.frame >= Bullet::ExplosionFrames.size())
               {
                    //Remove the bullet when its explosion animation has ended.
                    bullet.state = Bullet::Dead;
               }
          }

IMHO it should look so


          else if(bullet.state == Bullet::Exploding)
          {
               bullet.frame += framesToUpdate;
               if(bullet.frame >= Bullet::ExplosionFrames.size())
               {
                    //Remove the bullet when its explosion animation has ended.
                    bullet.state = Bullet::Dead;
               }
          }

or else you suffer from a problem: If the new frame comes too fast compared to DelayBetweenFrames, then framesToUpdate is 0, the branch is skipped, and the branch performing the sprite movement is entered instead. The inner part of "Exploding" works well even if framesToUpdate is 0 (I think we can neglect to check for negative values here, or insert an assertion is wanted), so no inner condition needed.

...and the branch performing the sprite movement is entered instead.

Oops, good catch - totally missed that. smile.png

Hi hyperfine,

Firstly, I would suggest moving controlling and updating the bullet outside the hero class as a bullet anda hero are totally separate (besides, won't you have enemy bullets too?). Servant's suggestion is a good one.

However, I would probably go with a slightly different approach, in that the "Bullet" class would represent a single bullet, and would contain all the physics and imaging required of it (ie, velocity, animation images, damage, owner, etc.). I would have a generic function that moves the bullets, and, check for collisions between any bullet and an object. If that collision occurs, I would delete the bullet from the list (vector) an add an Explosion object to the list of objects on screen, and it would play out itself. Once the explosion has finished, I would delete it as well.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

Sorry for my delayed response, thanks for all the replies! I'm using the approaches that you guys suggested with one minor difference. I have a vector called "ParticleList", which is a vector containing pointers to the bullet/particle class rather than variables of the class itself. It seems like this is necessary, since I want the vector to contain any of the different types of bullets/particles I'll have.

This topic is closed to new replies.

Advertisement