I encountered a similar problem while working on a project of mine. The player was able to pick up a certain variety of weapons, which shoot different type of bullets. I'll help you to figure out a solution for you by explaining the solution I found for my game.
In order to take advantage of inheritance and polymorphism, I managed to work out all the possible bullets my weapons were supposed to shoot, and made a list of their properties:
- Travel speed
- Impact damage
- Splash damage
- Graphic appearance
- Trail appearance
- Affected by gravity
First thing I did was to create the main parent class
Bullet, through which I could track and deal with any bullet object in my side-scrolling 2D shooter game.
Does the bullet physically travel in the space (projectile), or is supposed to reach the target instantaneously (hitscan)? Here we have two different behaviors: given starting point A and ending point B, our bullet may travel and get to B after a certain amount of time (remember the equation of motion), or get there after a single in-game timestep, eventually stopping before B because of a collision with an object. Do I need two different classes
ProjectileBullet who extends the parent class
Answer was no. Reason was: even though hitscan bullets reach their final position instantaneously, the algorithm I used to compute such position was based on the idea behind the binary search on arrays. Simply, I check if there are collisions with any instance of, for example, objects
BipedEnemy; if no, then I found the final position (the second point of my line); else, the middle point of trajectory is taken in account, then I verify if the first middle segment intersects any object: if yes, iterate; else, the collision occurs in the further segment, and I iterate anyway.
This algorithm requested two arguments: a starting position
(x,y), a direction
θ, and a
range, which are fields in my parent class, so I can edit them on need (and, we'll see I need to do so...). Because of the range being a finite number, I realized I could set this value as big as I liked to simulate
HitscanBullets (the bullet would eventually reach its target outside the screen, so no problem), and if target was visible by the player the effect was immediate.
On the other hand, a
ProjectileBullet could simply use a very smaller value, taking care of iterating such algorithm every timestep in order to give the idea of non-instant motion.
So, no separated classes here, I just pass a valid value for bullet speed (the
range argument) and get any motion effect I desire. Motion is done, and is put in the parent class
Bullet along with fields to track position, direction, and speed.
Nothing special to talk about here. Impact damage is another field (we may call
damage) in the parent class
Bullet, and stores a value which is subtracted to the object's health it collide with when moving. If that has health, obviously. Enemies have health, floor and walls don't.
Splash damage occurs when a bullet impacts, and delivers area or volume damage to nearby objects. In my game, I decided not to let a
Bullet deal with splash damage itself, instead it creates an object
SplashDamage, or maybe
DamageExplosion, which is in charge of delivering damage to objects closer than a suitable
Anyway, if you want your
Bullet to deliver splash damage on his own, you can consider a maximum distance for it. At this point, your
Bullet will have a
impact_damage (previously simply
damage), and two more fields,
splash_damage_range. Make sure to deliver less splash damage as distance increase from the bullet collision point.
If you implement splash damage as creation of a new object in charge of it, you can do a lot of different things: you may create explosions for missiles, minor explosions for energy-based bullets, or an electric area damage (as you mentioned). You could even burst new bullets at random directions (as you mentioned, I guess), from this point on.
Homing is the capability of a bullet to adjust its
direction to automatically move towards an enemy. Here I questioned what were the right criteria to move the bullet. Should it move to the closest enemy continuously? Or it should check for the closest enemy at creation and then just follow that particular one, regardless of others as it travels? I opted for the latter, as it was easier to implement.
Homing requires two operations: (1) identify the target, and (2) adjust the direction accordingly to its relative position. Can homing be implemented as a function, or is it better creating a subclass
HomingBullet which extends parent class
Bullet? Your choice. In my game, I extended my parent class with a subclass, because there was more than one bullet type capable of homing towards an enemy.
Does the bullet need to be destroyed at the impact, or the bullet can stick into walls, so it would be nice to see it? A rifle bullet is the first type, but if you fire a nail gun you expect to see nails sticking to walls or even enemies. I found it simple to implement this feature as a timer i called
bullet_life, being decreased every timestep, and when it reaches the
0 then I can destroy the bullet object. If you already set it at
0 the bullet is never seen once impacted a target or a wall.
Graphic and tail appearance
You may want to store the drawing sprite or model associated to a given
Bullet in a variable, or you can take advantage of inheritance for advanced drawing hierarchy. Anyway, drawing can be performed in many ways, so I'm not taking it in account for this answer.
This property can be implemented in the physics update functions, and I don't believe you need an entire class to make distinctions between bullets affected by gravity or not.
This is a possible way to realize your bullet hierarchy, but it isn't the only possible solution. Don't try to find the best solution at all, but the best one which fits your game: you don't really need a bullet capable of hitscanning, splash damage, bursting, electric... Well, it would be OP, ahah. Anyway, you can have a couple of subclasses to differentiate the unique properties, and by unique I mean that feature is not available to other bullets. If you want properties available to any projectile, then it would be a property of the parent class.