Log In  
Follow
moechofe
SHOW MORE

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)

P#49388 2018-02-18 12:05 ( Edited 2018-02-18 17:05)

SHOW MORE

Pico8 use 2 functions to update stuff at a regular basis. One _update() function called every 1/30s (or 1/60s since the 0.1.8 version) and one _draw() function that do the same thing.

That allow, for a project use that use the second as a time unit, to not slow down that unit.

From manual: _draw() is normally called at 30fps, but if it can not complete in time, PICO-8 will attempt to run at 15ps and call _update() twice per visible frame to compensate.

e.g., If I decide to make a sprite move from A to B in exactly 1 second but have a render process slower than 1/30s. I can put the move process in the _update function and the rendering in the _draw() function. The animation will not be smooth but the sprite will move from A to B in exactly 1 second.
This is true if the _update() function do not take more than 1/30s to execute.
This simple separation is only used for that purpose.

For my shmup project (bullet hell/danmaku) project, it's different.

The collisions are not physically computed because it will be too slow. It's a very simple collision system that use pixel color. At each frame, if a bullet is draw on the player ship, a collision will be produced.

Using a pixel-based collision system as a caveat: If a bullet move down at 3 pixel speed and the ship move up at 2 pixel speed. They can cross each other without generate collision. Why it is like it? Because the main purpose of a my shump is not about dodging a single bullets but a wall of hundreds. It will happen for a player to cross over a bullet without touching it but he will probably hit the following one. So it's ok.

That explained, It will be unfair to compute the collision and the rendering at two different speed using the two available functions _update() and _draw(), because player ship would collide with bullets that as not be rendered. To avoid that, everything related to the gameplay is in the _draw() function, collision and rendering.

What it change? It did not produce a time-based game but a frame-based game.

P#46003 2017-11-08 05:33 ( Edited 2017-11-08 10:33)

SHOW MORE

Cart #38150 | 2017-03-11 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

P#38151 2017-03-11 03:10 ( Edited 2017-03-12 04:29)

SHOW MORE

For my shmup project (bullet hell/danmaku), my first step was to work on the bullet/pattern system because it's related to the core of the gameplay (dodging bullets); and also because it's accountable to the beauty of the game.

First, I need limits. I decide to handle 128 bullets at a time. New bullets will replace the old one. From my perspective, is not a big deal. Players probably won't notice it. But if it happens, maybe it's because that bullet stays visible too long on the screen, turning arround following a circular path or simply moving too slowly. So, fuck that bullet.

I decide to create a table of bullet objects during the _init() proccess and reuse them instead of creating/deleting every time. It is called object pool

The process: every time I need to shoot a bullet, I select the next bullet object in the pool, If I reach the last one, I loop to the first. Simple.

To do that, I need:

  • a list of bullet objects,
  • an index for the next bullet index to use
  • and a number of max bullet:
bullets={}
bullets.next=1
bullets.len=128

Notice how the bullets table is used as an indexed list (to store bullets objects) and an object (to store properties).

During the _init() process, I warm up the table by creating all the bullet objects:

for i=1,bullets.len do
  local b={}
  bullets[i]=b
  ...later... -- warmup bullet properties
end

Next, every time I want to shoot a new bullet:

function shoot(...later...)
  -- get the bullet object
  local b=bullets[bullets.next]
  -- increment the next property
  bullets.next=(bullets.next%bullets.len)+1
  ...later... -- setup bullet properties
end

This little code make the .next property goes from 1 to 128 and loop to 1. Remember, tables in lua do not start at 0, but 1.

To draw the bullets:

for i=1,bullets.len do
 local b=bullets[i]
 spr(...later...)
end

I'm using a simple for loop here and not a for all() because I need the index for other purpose.

I'm using this technique for every data objects in my shmup game: bullet pattern functions, enemy ship, particles
With a little subtlety for the particles, because the objects are render sequentially, particles would appear stacked on each other. To fix that, when I need to create a new one, I do not increment by 1 but 7:

particles.next=(particles.next+6)%32+1

That's all for today, I hope you will use object pooling like crazy for your futur pico8 projects.

P#32758 2016-11-25 16:29 ( Edited 2016-11-25 21:35)

SHOW MORE

Because it's possible to add effects when drawing sprites like color switch and horizontal/vertical flip, I would like to be able to do it also with the map editor.
What do you think?

P#26723 2016-08-10 05:39 ( Edited 2016-08-11 08:18)

SHOW MORE

  • fully playable
  • difficulty rebalance
  • demo mode




P#15296 2015-10-12 04:20 ( Edited 2018-02-28 13:45)

Follow Lexaloffle:          
Generated 2024-03-19 01:53:43 | 0.071s | Q:18