|First, I can say the implementation depends on the feature wanted, but that’s also true for an entire game, right? So, nothing new.|
More specificly, here is a list of bullets related features and the relations needed to implement. All the proposals are obviously related to the choice I have made during my research and my 3 different versions. (Note: I did not implement all of the features listed.)
WANT: Bullets can change direction/speed after being fired.
NEED: A function is needed to do that and not a simple addition operation. Also a time or frame counter for each bullet to use as abscissa or to trigger the direction/speed changes.
WANT: Bullets can aim to a specific point on the field or the player ship.
NEED: The previous function needs access to the player ship properties: position but also direction, can be used to shoot bullets in front of the player (sweet)!
WANT: Bullets can shoot other bullets.
NEED: Bullets need to have access to the system that shoot bullets. (I discovered that a bullet/pattern system is highly not compatible with closed, encapsuled, POO, rigid... code. In my implementation: everything is public.) It also requires the code that shoots bullets, which must be compatible with a bullet source and its properties: position, direction, cannon size (see below).
WANT: Bullets can be cancelled by a player's bomb/lazer or by the ship that shoot at them.
NEED: A cancel using a shape require a blazing fast collision system and a lot of available CPU power, that is not the case in pico-8. An enemy that cancels all the bullets that it shot when it dies requires to store a reference or the index of each one. It was impossible for me to implement that because of the bullet pool I used (see below).
WANT: Fire a group of bullets using a shape: line, arc, circle, spiral... and not necessarily at the same frame.
NEED: An emitter function that take parameters that come from the patterns data: number of bullets to fire per frame, number of frames to wait before fireing again, number of times to repeat. Or any other combination of variables that can fit. I chose this one because it simplifies the code to shoot bullets. Also, it requires to keep the emitter in memory until it finishes or it has been canceled.
WANT: Fire a shape of bullets in a specific direction or aim to the player.
NEED: Emitter should have access to the player ship.
WANT: Specify the parameters for the bullets and the shapes.
NEED: There is a lot of different implementations to solve this problem. In a previous version of my code, I used weird structs with too much depth: bullet, move, shape, frame, weapon, fire, pattern. For my pico-8 game, I use a timeline with instructions and a big function that parse it and change the parameters of a state machine. This part requires another whole post to explain.
WANT: Shoot bullets at a shorter interval than the actual frame rate of the game. It was needed for me, when I wanted to fire a pattern in a spirale shape with a very small angle between each bullet that will force the player to avoid the wall of bullets by flying around the enemy. But I didn't re-implement it in pico-8.
NEED: Some headache! The idea was to simulate the delay of a 1/4 of frame by changing the position of each bullet comparatively to the speed and the delay. Not easy, but effective and impressive effect.
When I watch some videos of commercial great shmup games, I can see what I can copy and what I can’t. There's a tone of intersting things that I have not thought of a solution yet:
- allow ship to fire mutiple shape at a time.
- mixing the assets of the bullets inside a same shape.
- allow last shot bullets to catch up with the first ones. (cool effect)
- allow to create a shape of hundred of static bullets and then, throw them at the player all at once. (very cool effect)
- randomize position of bullets in shape (to create disorder effect)
- use a lot of bullets with a high speed, combined with the ability to shoot bullets at a higher speed than the frame rate (see above), to create an impassable wall.
- draw a bullet cancel effect, by changing the sprite, or spawn a particule.
- draw not rounded bullets only.
- allow to position bullets in a shape but shoot them in an other shape. Hard to explain but awesome effect.
- change the cannon direction using function or by aiming at the player.
Now, some tips for the implementation.
I'm using two structs with their respective function action: bullets are shot and shapes are fired. Here are the signatures of the two functions.
-- h= ship/cannon object -- c= bullet count (this frame) -- d= frame delay -- t= repeat x times -- n= shape function -- m= movement function -- s= speed -- f= sprite index -- w= aim follow mode function fire(h,c,d,t,n,m,s,f,w) ... end -- x,y= bullet position -- u,v= bullet direction -- f= sprite index -- m= movement function function shoot(x,y,u,v,f,m) ... end
Because of the limitation of pico-8, I use object pool everywhere. 512 bullets and 16 emitters. That means if I had to fire or shoot more, it will replace the ones created first.
About object pool https://www.lexaloffle.com/bbs/?tid=28114
-- t.g= next item index -- t.l= max number of items function next(t) t.g=(t.g)%t.l+1 end
I'm using what I call the cannon size. It's the distance from the cannon and where the bullet will appear. In my pico-8, it is not variable and I regret it. Basically it is used to make the patterns more beautiful by avoiding crowded effect.
For the collision between the player and the bullets, I'm using a fake distance without computing the square root. It reduces CPU usage and its barely noticeable.
function dis(x,y,u,v) m,n=u-x,v-y return m*m+n*n end
I'm updating, drawing and testing collision in the same loop. Also to save CPU cycle.
For the shape, I finally acheived to reduce to only one function that can fire bullets in a shape of arc, circle or spirale. It's a function that return a function. The inner one can use the local variable declared in the outer one. The outer parameters come from the timeline and the state machine, the inner parameters come from the code that fire bullets.
-- a= spirate angle? 0.5 for circle shape, 0 for a point -- s= spirale speed? I guess. function arc(a,s) ... -- i= index of the bullet in the pool -- q= the shape object -- j= the index of the bullet in the current rendered frame -- f= aim follow mode return function(i,q,j,f) ... end end
I'm abusing the use of bullet indexes to create some diversity. I like it to be more determinist than a random function.
I hope this long explanation can help you understand how I built my pattern system. Keep in mind that I iterate a lot and refactor a lot. My solution is far from perfect, and I will probably restart from scratch any day.
If you try to read the code, I have to confess, I started this project when the tokens count system of pico-8 did not exist. The limitation was based on the numbers of characters. That’s why a lot of my variables are 2, 3 or 4 letters long. It makes the code hard to read, even for me. (edited)
[Please log in to post a comment]