Moving an object in a spiral pattern. (Math inside!)

Hello folks, I really hope you can help me because I’m scrathing my head with this question.

I’m building a bullet hell shmup and I need some bullets to move in a sort of expanding spiral. To be clear: there is an initial bullet which, at some point, explodes in a number of smaller bullets and each of them should move in an expanding spiral.

Now, a spiral is pretty easy to describe by slightly modifying the circle parametric equation, the thing is I want to move my bullet by translating its position through a Vector3. Like:

void Update() {
Vector3 movement = new Vector3 (0, bulletSpeed * -1, 0);
        movement *= Time.deltaTime;
        transform.Translate (movement);
}

The problem is: how do I build a Vector3 which can describe an expanding spiral over time?

Thanks in advance!

So, spirals are easy if you think of them as drawing a circle, except that the circle’s diameter increases as you go around. And if you know how to draw a circle I guess. so let’s start there.

Sin and cos are your magic circle drawers. If you loop “theta” from 0 to 2π (radians), then you can set X,Y to cos(theta), sin(theta). That’ll trace a circle with a radius of 1.

Turning that into a spiral is easy: multiply your vector by theta. e.g. Vector2(cos(theta), sin(theta) ) * theta. If you keep looping past theta in your loop, you now have a spiral that will continue outward as long as you need it to.

This doesn’t give you a whole lot of control over the shape of the spiral, so be prepared to throw in multipliers and additions at different parts of that. A multiplier just tacked onto the end will make it spiral out slower (tightening up the spiral), for example. If you want the ability offset the angle (which you probably will for your bullet hell), then make it sin(theta+offset)

And of course you’ll set your Z to some multiple of theta as well.

One thing I’d adjust with your concept: The above formula only really works as a direct position setter, not a “mover”. So you’ll want to record the starting position in Start() and then set it via transform.position = result rather than transform.Translate. (I’m not sure there is a reliable formula for this shape to be used in .Translate - everything I can think of would result in tiny offsets with framerate variations, which make bullet hell games look like shit… you want those beautiful precise patterns, and this is how you get them)

In that case, you’ll want to essentially set theta to (time - startTime), and that should give you the parameter you need to math out everything else.

2 Likes

I would do it exactly as @StarManta describes, abandoning your desire to use Translate and just use the parametric equation.

But if you must do it with local translations for some reason, then this procedure will work:

  1. Give your bullets a “turn rate” property, which is how fast they turn (in, say, degrees/sec). Start this out with a quite high value (and with your bullets evenly or randomly distributed in orientation).

  2. Have your bullets always move forward (something like transform.Translate transform.forward * speed * Time.deltaTime — or in a 2D game, you might use transform.right instead of transform.forward).

  3. Have them also rotate according to the turn rate.

  4. Finally, reduce the turn rate by a small amount per frame.

That will produce a spiral, and by fiddling with the speed, turn rate, and how quickly the turn rate is reduced, you should be able to get the shape you want.

I’d be concerned with this approach that tiny differences in frame timing could build up to make the spiral patterns look irregular. Imagine one frame takes an unusually long time: with that approach, the bullet would go too far “forward” in that one frame before turning, and would get out of alignment with the other bullets. In most cases a small difference like that wouldn’t be noticeable, but picture that in a pattern of bullets akin to this one:

The gap would stand out like a sore thumb.

The only way I’d recommend using Translate would be if it was going with FixedUpdate… which of course can have its own compounding performance issues if the framerate starts to drop.

Yep, agreed on all points; the parametric approach does not accumulate error, while any method of updating-as-you-go does. And cumulative error is generally a thing you want to avoid.

But, since the OP had apparently already considered that and wanted an as-you-go method, I thought I’d offer help with that anyway.

Thanks for answers! They gave me the correct input. Now that you mention it: I have a number of bullets that are moving using Translate, like in my example. Is that code reasonable for “simple” bullets who are just blindly going from Y +1 to Y -1? Or should I replace that code with something like transform.y += (bulletSpeed * -1) * Time.deltaTime; ? Precision should be a priority in a bullet hell game.

It’ll be less of an issue because they’re not turning, but I would say to use absolute formula positions just to be on the safe side. That wouldn’t be adding to the transform position, but absolutely setting the position as described in the spiral one. (You could actually use the exact same spiral-bullet code just with 0’s for the spiral sizes and such, if you wanted.)

One note is that when firing a bullet, the exact time it was fired in the ideal case might not be exactly Time.time. If your enemy fires bullets every 0.05 seconds, and two frames are 0.03 seconds apart, those won’t divide evenly, so you’ll end up with bullets fired at (for example) 0.03, 0.06, 0.12, 0.15… Makes for a lot of visually staggered bullets, and that’s no good. Normally sub-frame timing is no big deal, but this is a special case. (and heaven help you if you have one slow frame that lasts 0.15 seconds and actually skip bullets being fired!)

The way I solved this was to track my time in my “gun” script, and make sure that the bullets know that they started at a particular sub-frame time. For example:

private float myTrackedTime = 0f;
private float firingInterval = 0.05f;
void Start() {
myTrackedTime = Time.time;
}
void Update() {
while (myTrackedTime < Time.time) {
GameObject bulletGO = Instantiate(bulletPrefab);
Bullet bullet = bulletGO.GetComponent<Bullet>();
bullet.startTime = myTrackedTime;
bullet.startPosition = gunBarrel.position;
myTrackedTime += firingInterval;
}
}

(And if you do this, DON’T set startTime to Time.time in the Bullet’s Start function)
If your Bullet script uses its own startTime as set in the above code as the base for the position setting formulas, then they’ll be lined up at precise intervals and look gorgeous.

1 Like