12
\$\begingroup\$

I want to make a series of expanding bullet patterns that form shapes such as squares, triangles, etc. An example of what I'm after can be seen in the following video where when the stars are collected the bullets explode in the shape of an expanding star:

https://youtu.be/7JGcuTWYdvU?t=2m41s

\$\endgroup\$
  • 2
    \$\begingroup\$ Oh that's a good question. I don't have a specific answers, but I'd imagine you could use a 2D object, either a sprite or simple shape, and spawn the bullets along the edge. Of course, the trick would be giving them proper velocity, both outward in their shape and if you are making a scroller like this, to make them move forward with the screen. Very interested to see any answers here. \$\endgroup\$ – Jesse Williams Jul 27 '17 at 14:48
  • 1
    \$\begingroup\$ A popular name for that kind effect is "particle effects." That search term may help you out! \$\endgroup\$ – Cort Ammon Jul 27 '17 at 19:16
  • 1
    \$\begingroup\$ Thanks, I have been using particle effects in XNA and libGDX for quite a while now, but wasn't sure how to handle this particular style of effect. \$\endgroup\$ – lepton Jul 27 '17 at 19:33
  • 1
    \$\begingroup\$ There's another answer to this that is incredibly powerful, but very complex to program. And is need a real keyboard to type out. Bookmarking this for later explanation. \$\endgroup\$ – Draco18s Jul 27 '17 at 22:24
  • \$\begingroup\$ Interesting - I never would've gone with particle effects for something like this. Or maybe it's just a delineation in Unity. While particle effects can have colliders (thus damaging an object), it seems like that would create a lot more overhead than simply instantiating object copies. \$\endgroup\$ – Jesse Williams Jul 28 '17 at 13:44
11
\$\begingroup\$

The easiest way to do this would be to first design the shape then calculate the movement of the particles. In this answer I will be constructing a square, but this applies to any shape.

Start by designing your shape as relative positions around some origin point.

square

Now you need to calculate how the shape will expand. To do this we simply calculate the vector pointing from the origin to every point by subtracting the origin position from our point's position then normalizing the vector. vector = normalize(point.x - origin.x, point.y - origin.y).

vector

We can now calculate the position of the points at any point in time by using this vector. You calculate the next position of the points by doing point.position += point.vector * point.velocity. Pseudocode example using our previous point:

// When you start your program you set these values.
point.position = (-3, 3); // Start position. Can be anything.
point.vector = normalize(-3, 3); // Normalized vector.
point.velocity = 3; // Can be anything.

// You do this calculation every frame.
point.position += point.vector * point.velocity;
// point.vector * point.velocity = (-3, 3)
// point.position is now (-6, 6) since (-3, 3) + (-3, 3) = (-6, 6)

Doing this will move all points outwards at 3 units every frame.


Notes

  • You can read up on some simple vector maths here.
  • The position can be anything as long as all positions are relative to some origin point.
  • The velocity of all points should be the same to ensure uniform movement, but having different velocities might give you interesting results.
  • If the movement seems off you should check the origin point. If it's not in the exact middle of the shape the shape might expand in a strange way.
\$\endgroup\$
  • 9
    \$\begingroup\$ I just want to point out, the velocity of each particle should be proportional to the distance from the origin on the first frame (meaning only calculate once not per frame). Alternatively, you could simply not normalize the direction vector. If you don't do this, the shape will not scale linearly, but rather move towards becoming a circle (if all velocities are the same.) \$\endgroup\$ – Aaron Jul 27 '17 at 18:10
  • \$\begingroup\$ @Charanor Many thanks for the explanation. I actually studied discrete mathematics at university but it was quite a while ago now. I'm going to try and implement something today. \$\endgroup\$ – lepton Jul 28 '17 at 16:02
2
\$\begingroup\$

So, there's this project out there called BulletML which is a markup language for creating complex particle / bullet patterns. You'll almost certainly need to port the code to your own language, but it can do some really amazing things.

For example, this boss was done in a (heavily modified) extension of BulletML for Unity3D (the author of that pattern uploaded that video and Misery is insane, as well as good1). It's the most difficult variation of that enemy and it shows off what BulletML is capable of quite well (and check out some of Misery's other bosses, too, Like Wallmaster).

Or I can show of this example, which is a pattern I wrote while working on an expansion for The Last Federation, using an older revision of the system that's less mod-friendly and uses only single character A-Z variables:

Bullet pattern example

The green bullets making those rings there are spawned from a parent bullet that rotates at high speed, but themselves have no motion. They deal massive damage, keeping the player at a longer range, restricting them to lower damage weapons and allowing mobile defenders to harass the player (the player won if the unmoving structure in the middle there was destroyed).

Here's a portion of the XML syntax that creates those bubbles:

<bullet_pattern name="Barrier">
    $WallShotAngle B=.3 A=90
    $WallShotAngle B=.3 A=-90
    $WallShotAngle B=.3 A=0
    $WallShotAngle B=.375 A=180
</bullet_pattern>

<var name="WallShotAngle">
    <bullet angle="[A]" speed="4000" interval_mult=".01" dumbfire="1" shot_type="GravityWavePurple">
        <wait time="[B]" />
        <change angle="0" speed="1000" time=".0001" />
        <spawn>
            <bullet_pattern>
                <bullet angle="[A]" speed="0" shot_type="CurveBarGreen" damage_mult="8">
                <wait time="12" />
                <die />
                </bullet>
            </bullet_pattern>
        </spawn>
        <die />
    </bullet>
</var>

You can see some of the purple "gravity wave" shots in the screenshot, which travel almost instantly from the source (which rotates) to the bubble's edge, whereupon it spawns the green "curved bar" shot, which sits there for 12 seconds before despawning. The blue and yellow shots I've omitted, as they are much more complicated.

One of the other patterns (an artillery shell) in the expansion was actually written by Misery, although I made some modifications to it. Initially it's a low-damage, penetrating shot that flies out to long range and then explodes into a huge fireworks display, dealing tons of damage. It's max range was much higher than the player could achieve, essentially forcing the player to engage at short range, which was advantageous for the other types of NPC units due to the shotgun effect (more bullets clustered in a small zone).

BulletML is easy to work with, generally, and can do amazing things. Bullets can change direction, change speed, spawn other patterns, die early, repeat collection of commands in a loop, use delays, change bullet sprite image, follow their parent (or not)...And anything it doesn't support you could write into it.

I'd definitely recommend it if you're doing a serious shoot em up game. You would still need to work out the coordinate math to get the desired shapes, as Charanor talks about in his answer, but a bullet engine like BulletML will give you so much more flexibility that you'll spend more time designing new patterns than figuring out how code them.

  1. To explain just how good Misery is, those videos are against floor bosses with starting equipment: no modules, no consumables, and the basic pea shooter. And xe only takes one hit despite the elongated nature of the fight. Ok, 9 hits against Centrifuge (who doesn't show up until the third floor after the player will definitely have upgrades resulting in at least double damage comparatively).
\$\endgroup\$
  • \$\begingroup\$ Thanks, I was vaguely aware of BulletML, as it's been around for a while, but it's definitely overkill for my simple game, which only occasionally dabbles in bullet-hell, and is not a bullet-hell shooter per se. \$\endgroup\$ – lepton Jul 28 '17 at 18:37
  • \$\begingroup\$ @lepton Totally understandable. That's a decision for you to make, but the answer may be the "best" one for someone else. I know that after working on TLF and went on to start building my own shooter I wanted to use it just because of how powerful and easy it was to work with. :) \$\endgroup\$ – Draco18s Jul 28 '17 at 18:40
1
\$\begingroup\$

As pointed out by Charanor you can use an array of points to define your shape and then update their position over time. Below is a working example of how to implement a star shape or a custom shape using points:

package com.mygdx.gtest;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;

public class Test extends ApplicationAdapter{

    public SpriteBatch sb;
    private StarShape ss, ssBig;

    @Override
    public void create() {
        sb = new SpriteBatch();
        Pixmap pmap = new Pixmap(2, 2,Format.RGBA8888);
        pmap.setColor(Color.WHITE);
        pmap.fill();
        ss = new StarShape(50,50,new Texture(pmap), 10, true);
        ssBig = new StarShape(250,250,new Texture(pmap), 50, false);
        pmap.dispose();

    }


    @Override
    public void render() {
        super.render();

        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        ss.update(Gdx.graphics.getDeltaTime());
        ssBig.update(Gdx.graphics.getDeltaTime());

        sb.begin();
            ss.draw(sb);
            ssBig.draw(sb);
        sb.end();

    }


    @Override
    public void dispose() {
        super.dispose();
    }

    private class StarShape{
        public float progress = 1f;
        public Texture bulletTex;
        public Array<Vector2> points = new Array<Vector2>();
        public Vector2 center;

        public StarShape(float x, float y, Texture tex, float initialSize, boolean mathWay){
            center = new Vector2(x,y);
            bulletTex = tex;

            if(mathWay){
                // define star shape with maths
                float alpha = (float)(2 * Math.PI) / 10; 
                float radius = initialSize;

                for(int i = 11; i != 0; i--){
                    float r = radius*(i % 2 + 1)/2;
                    float omega = alpha * i;
                    points.add(
                            new Vector2(
                                    (float)(r * Math.sin(omega)), 
                                    (float)(r * Math.cos(omega)) 
                                )
                            );
                }
            }else{
            // or define star shape manually (better for non geometric shapes etc

                //define circle
                points.add(new Vector2(-3f,0f));
                points.add(new Vector2(-2.8f,1f));
                points.add(new Vector2(-2.2f,2.2f));
                points.add(new Vector2(-1f,2.8f));
                points.add(new Vector2(0f,3f));
                points.add(new Vector2(1f,2.8f));
                points.add(new Vector2(2.2f,2.2f));
                points.add(new Vector2(2.8f,1f));
                points.add(new Vector2(3f,0f));
                points.add(new Vector2(2.8f,-1f));
                points.add(new Vector2(2.2f,-2.2f));
                points.add(new Vector2(1f,-2.8f));
                points.add(new Vector2(0f,-3f));
                points.add(new Vector2(-1f,-2.8f));
                points.add(new Vector2(-2.2f,-2.2f));
                points.add(new Vector2(-2.8f,-1f));

                // mouth
                points.add(new Vector2(-2,-1));
                points.add(new Vector2(-1,-1));
                points.add(new Vector2(0,-1));
                points.add(new Vector2(1,-1));
                points.add(new Vector2(2,-1));
                points.add(new Vector2(-1.5f,-1.1f));
                points.add(new Vector2(-1,-2));
                points.add(new Vector2(0,-2.2f));
                points.add(new Vector2(1,-2));
                points.add(new Vector2(1.5f,-1.1f));

                points.add(new Vector2(-1.5f,1.5f));
                points.add(new Vector2(1.5f,1.5f));

            }

        }

        public void update(float deltaTime){
            this.progress+= deltaTime;
        }

        public void draw(SpriteBatch sb){
            Vector2 temp = new Vector2(0,0);
            for(Vector2 point: points){
                temp.x = (point.x);
                temp.y = (point.y);
                temp.scl(progress);
                sb.draw(bulletTex,temp.x + center.x,temp.y +center.y);
            }
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Big thanks for the example, I'm going to check it out this afternoon to see if I can get it up and running. \$\endgroup\$ – lepton Jul 28 '17 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.