Even spread of projectiles originating from the weapon (that shoot at the same angle)

POCKETS

Terrarian
I feel like this one may be more difficult than it sounds. Finding an example of an even or random spread of projectiles, that originate from the center of the weapon, is actually quite easy. So, I'm taking that code as an example to learn from.

I wanted to post what I've learned from the code so far, so that I can understand where to go from there.

OK, so here's the projectile spread code (this is a variation of what I'm currently using) -


C#:
for (int i = 0; i < 3; i++)

            {
                Vector2 perturbedSpeed = new Vector2(speedX, speedY).RotatedByRandom(MathHelper.ToRadians(12));
                Projectile.NewProjectile(position.X, position.Y, perturbedSpeed.X, perturbedSpeed.Y, type, damage, knockBack, player.whoAmI);
            }
            return false;

What I've learned from this so far -

To begin with, the for loop is basically telling the game how many projectiles to spawn.

Projectile.NewProjectile is responsible for spawning a new projectile. The position.X and Y tell the projectile where to spawn. In this case, the center of the weapon - more specifically, the center of the weapon as you rotate the weapon around 360 degrees - has to do with vectors. The speed X and Y also have to do with vectors, and I'm seriously lacking knowledge on this subject (I do know it has to do with direction and magnitude). The type is just the type of projectile (in my case mod.ProjectileType<modProjectile>()). The damage here can be set independently of the weapon's damage (basically, it will ignore weapon damage and bonuses) - if I'm wrong about this, just let me know, but this is how it was explained to me. Knockback is self explanatory. The player.whoAmI (I believe) just sets who shot it?? (not sure on this one).

If I'm wrong about any of this, please feel free to correct me.

Obviously, these new projectiles will always originate from the center of the weapon, so that's not what we want.

I feel like this problem has to do with vectors, from what little I know of them, and from what I've seen in very basic YouTube video explanations on the subject.

Now, here's where I try to explain how stupid I am (this is more or less just thinking out loud) --

Alright, so let's say that when the weapon is rotated (which can rotate 360 degrees), it's sprite (or image) has to move around a center point (we'll call this X0 and Y0). Obviously, the weapon sprite doesn't rotate around it's own center, it has to be rotating around some other point (perhaps the player?). Therefore, the sprite of the weapon must be offset from that point by a certain amount. So, if it's offset from the player's center, this make perfect sense, and I believe this has to do with vectors.

So, if I drew a line from X0, Y0 to Y10 and placed a point at each spot, then rotated that line using X0, Y0 as an axis, rotating by 90 degrees would eventually get me a line from X0, Y0 to X10 (or -X10, depending on which way you rotated). OK, so with that in mind, I think of a weapon's rotation around the player's center the same way. Therefore, using the above example, shooting a projectile straight up in the air, the projectile would originate from Y10.

I should point out that I have no idea if projectiles in Terraria use the player as the rotation point for the firing of said projectiles. It would make sense, but from what I've seen in the code, it may have to do with the weapon, so maybe the player's center is just a reference for the weapon and it's firing center? Just curious.

If I want to offset a projectile's originating position, I can't just change the center of rotation, as the projectile's firing position would appear to change as the weapon was rotated around 360 degrees (this is why offsetting from position.X and position.Y in NewProjectile won't work). There must be a way to still use the player's center (or whatever the weapon is using as it's rotational axis), and then just rotate the line slightly using that axis, making the projectile appear to be originating from an offset of the weapon's firing center. Then, rotate the projectile itself by a small amount to account for the change (otherwise, it would still appear to be shooting at an angle from the weapon itself).

Alright, so now that I've completely embarrassed myself, I will defer to the more experienced users to lend me a helping hand on this one. If I'm even close, I'd be surprised, but logically speaking, my explanation does make sense, at least in my head. Again, I have no clue if Terraria handles projectiles this way, so please feel free to correct me.

I'm the type of person that has to know how things work, so I can't just ask for code and be on my way. I need to know how things are functioning :)
 
Last edited:
You're pretty much on the right track here. The good news however is that we can (ab)use existing Terraria/tModLoader methods to get what you want far easier than if we have to do everything from scratch.

The linchpin of pretty much anything regarding shooting projectiles is the aptly named Shoot method; even if we won't actually allow it to shoot its projectile, we can still use the parameters it takes to great effect. The Shoot method signature is as follows:
C#:
virtual bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
As you might have noticed, a lot of the parameters of Shoot also appear in NewProjectile. This is because Shoot is essentially a wrapper for NewProjectile, with as argument for speedX and speedY the direction towards the cursor. This is incredibly useful for aligning shots to a muzzle, because your weapon will be pointing in the same direction. So all you need to do is to fire bullets from a muzzle is to add a muzzleOffset (you'll have to determine this experimentally) to position. In code, this could look like this:
C#:
override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
{
    Vector2 muzzleOffset = new Vector2(5, 10) * Vector2.Normalize(new Vector2(speedX, speedY)); // These are random numbers, you'd have to determine those by tweaking and finding out what looks good.
    position += muzzleOffset;

    // Shoot bullets here using the new position and using speedX and speedY

    return false; // Stop the weapon from shooting a bullet because we're spawning them manually.
}
The rest of the weapon is pretty much the for loop you already posted. If you insert that at the marked place, the result should be pretty close to what you're looking for. If you get stuck, ExampleMod has the ExampleGun item, which showcases a lot of nifty things you can do with Shoot. :)
 
It's probably my fault for rambling on and on in my OP. I should have just gotten to the point and been more clear about what I was trying to accomplish.

Right now, the weapon has a firing center where each projectile is fired from (as seen in the top example in the image below). What I am trying to do is make it so that each individual projectile is fired from an offset of that center position, but fired straight, not an an angle (as seen in the bottom example).

The muzzleOffset code example you posted seems to only change the distance from the weapon's firing center (more specifically, the weapon's sprite). Each of those numbers has to be the same, or you end up with the group of projectiles firing from different positions as the weapon is rotated. Perhaps my implementation is incorrect, though.

Here's an image of what I'm trying to accomplish (again, my apologies for my lack of drawing skills. I can actually draw, but this is just a quick and dirty PS job) -




Firing Positions.png



I don't need to change where the center of the group of projectiles are firing. I need to offset each individual projectile from the weapon's original firing center. I feel like I may have missed an examplemod that had this exact thing, but I can't, for the life of me, find where it is...

Anyhow, I'm really sorry that I didn't include that image in my OP.

I believe that if I can get the offsets correct for each projectile, I can remove the for loop entirely, and then just create three instances of projectile.NewProjectile, and use different offsets for each instance
 
Last edited:
Ah, I see! That wasn't clear to me. If that is what you want, we'll only have to make a few modifications:
C#:
override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
{
    int projectileCount = 3;
    float xOffset = 5; // This is the horizontal offset, which is the same for every bullet;
    float yOffset = ((projectileCount - 1) / 2f) * -spread; // The starting value is where the first bullet starts. We'll take care of the rest in the for loop.
    float spread = 10f; // The amount of distance between bullets.
   
    position.X += xOffset;
    position.Y += yOffset;
   
    for (int i = 0; i < projectileCount; i++)
    {
        Projectile.NewProjectile(position.X, position.Y, speedX, speedY, iCantReallyRememberTheRestOfTheParametersSoThisWillHaveToDo);
        position.Y += spread;
    }

    return false; // Stop the weapon from shooting a bullet because we're spawning them manually.
}
This should get what you need (with the usual disclaimer that I made this up on the spot and hasn't been tested). :)
 
I think we're definitely getting close here.

This only works if you shoot the projectiles in a straight line in the X plane. If you start rotating the weapon up or down, the projectiles will eventually all start firing in the center again. They start to merge with each other.

In my original post, I mentioned offsetting the X and Y positions in NewProjectile, and how that didn't work because even if I offset those X and Y positions, as the weapon rotates around, the firing positions will appear to change.

I'll try to explain what's happening visually -

Firing Positions #2.png


Obviously, these are exaggerations to show what's happening to each projectile. Once the weapon fires straight up, all the projectiles appear to be in line with each other.

Again, I feel like we're definitely close.
 
Last edited:
I think we're definitely getting close here.

This only works if you shoot the projectiles in a straight line in the X plane. If you start rotating the weapon up or down, the projectiles will eventually all start firing in the center again. They start to merge with each other.

In my original post, I mentioned offsetting the X and Y positions in NewProjectile, and how that didn't work because even if I offset those X and Y positions, as the weapon rotates around, the firing positions will appear to change.

I'll try to explain what's happening visually -

View attachment 227558

Obviously, these are exaggerations to show what's happening to each projectile. Once the weapon fires straight up, all the projectiles appear to be in line with each other.

Again, I feel like we're definitely close.
Whoops, should've seen that coming. Took me a while to figure out how to resolve it, and it turns out simply rotating around a point after all (à la Tsunami) is the best way to go about it. The main difference is that projectiles now spawn in an arc, rather than in a straight line. Let me know if that's not what you want, I'll try to figure out how to best solve it.
C#:
override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
{
    int projectileCount = 3;
    float startRadius = 5f; // This is how far from the center the bullets will start.
    float spread = (float)Math.PI / 18f; // The angle between bullets in radians (PI = 180 degrees).
   
    Vector2 direction = Vector2.Normalize(new Vector2(speedX, speedY));
    direction *= startRadius;
   
    for (int i = 0; i < projectileCount; i++)
    {
        float angleStep = (float)(i - (projectileCount - 1)) / 2f;
        Vector2 firePosition = direction.RotatedBy(angleStep * spread);
       
        Projectile.NewProjectile(firePosition, speedX, speedY, iCantReallyRememberTheRestOfTheParametersSoThisWillHaveToDo);
    }

    return false; // Stop the weapon from shooting a bullet because we're spawning them manually.
}
 
I don't know if I'm doing something wrong, or if I'm missing something...

C#:
        public override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
        {
            ++shootCounter;

            int projectileCount = 3;
            float startRadius = 5f; // This is how far from the center the bullets will start.
            float spread = (float)Math.PI / 18f; // The angle between bullets in radians (PI = 180 degrees).

            Vector2 direction = Vector2.Normalize(new Vector2(speedX, speedY));
            direction *= startRadius;

            for (int i = 0; i < projectileCount; i++)
            {

                float angleStep = (float)(i - (projectileCount - 1)) / 2f;
                Vector2 firePosition = direction.RotatedBy(angleStep * spread);

                Projectile.NewProjectile(firePosition, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
            }


Projectile.NewProjectile(firePosition, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);

This line is giving me trouble. firePosition appears to be a vector, and it looks like it's expecting two vectors (perhaps an X and Y?). Without two vectors there, the speedX and speedY are giving me errors that tell me I cannot convert from float to vector for speedX, and cannot convert from float to int for speedY.

I tried a whole host of different things to try and make it work, but I had no success. I tried using "direction" in place of speedX and speedY, but had no luck.
 
I don't know if I'm doing something wrong, or if I'm missing something...

C#:
        public override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
        {
            ++shootCounter;

            int projectileCount = 3;
            float startRadius = 5f; // This is how far from the center the bullets will start.
            float spread = (float)Math.PI / 18f; // The angle between bullets in radians (PI = 180 degrees).

            Vector2 direction = Vector2.Normalize(new Vector2(speedX, speedY));
            direction *= startRadius;

            for (int i = 0; i < projectileCount; i++)
            {

                float angleStep = (float)(i - (projectileCount - 1)) / 2f;
                Vector2 firePosition = direction.RotatedBy(angleStep * spread);

                Projectile.NewProjectile(firePosition, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
            }


Projectile.NewProjectile(firePosition, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);

This line is giving me trouble. firePosition appears to be a vector, and it looks like it's expecting two vectors (perhaps an X and Y?). Without two vectors there, the speedX and speedY are giving me errors that tell me I cannot convert from float to vector for speedX, and cannot convert from float to int for speedY.

I tried a whole host of different things to try and make it work, but I had no success. I tried using "direction" in place of speedX and speedY, but had no luck.
Whoops, overestimated the overloads.
C#:
Projectile.NewProjectile(firePosition.X, firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
That should work. :)
 
C#:
Projectile.NewProjectile(firePosition.X, firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);

Interestingly enough, I actually tried this. While the code itself gives me no errors, there are no projectiles being fired from the weapon.

The weapon looks completely normal in game. It even "appears" to be firing, and rotates as I move my mouse, but no projectiles are being fired. It's very strange.

EDIT: I just wanted to add that I checked to ensure that the code was even being run, so I set a breakpoint at the newProjectile line, and sure enough, it does. The game stops right when I fire the weapon, but no projectiles are showing up on screen. It's like they're not even being drawn...

EDIT2: ModProjectile is not actually the name of my projectile. I just used that as a placeholder ;-)
 
Last edited:
C#:
Projectile.NewProjectile(firePosition.X, firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);

Interestingly enough, I actually tried this. While the code itself gives me no errors, there are no projectiles being fired from the weapon.

The weapon looks completely normal in game. It even "appears" to be firing, and rotates as I move my mouse, but no projectiles are being fired. It's very strange.

EDIT: I just wanted to add that I checked to ensure that the code was even being run, so I set a breakpoint at the newProjectile line, and sure enough, it does. The game stops right when I fire the weapon, but no projectiles are showing up on screen. It's like they're not even being drawn...

EDIT2: ModProjectile is not actually the name of my projectile. I just used that as a placeholder ;-)
I found the issue: me being a colossal idiot.
C#:
Projectile.NewProjectile(position.X + firePosition.X, position.Y + firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
Maybe I should start testing my code... :confused:
 
That does produce projectiles. So that's a good thing ;-)

I see what you mean about being in an Arc, which is... not really what I had intended, although it does look kinda cool.

I figured out that the only way to get the projectiles further apart from each other was to set the startRadius to something higher, and the spread float number to something lower than 18f. Right now, I'm using a startRadius of 25f, and a spread of 0.8f. That seems to be a good mix.

I was hoping for a straight line, but I'm very grateful for the help you've given me here. I suppose this is where I tell myself beggars can't be choosers :)

This only seems to work well with three projectiles. Using two, or even four projectiles, I can't get the spread to center properly.

I'd love to get this to work in a straight line, but you've been such a very big help so many times, I hate to keep pestering you.

EDIT: Success!

OK, so this may be a completely unorthodox way of doing something like this. The for loops are probably not even necessary since I'm only creating 1 projectile per instance of the loops, although I'd have to create new variables for certain things in the second instance (I think), but that's minor.

I'm now using a variation of your code to get just two projectiles evenly spaced apart (if there's a much better way to do this, just let me know, and I'll adjust my code accordingly) -


C#:
            int projectileCount = 1;
            float startRadius = 15f; // This is how far from the center the bullets will start.
            float spread = (float)Math.PI / 18f; // The angle between bullets in radians (PI = 180 degrees).

            Vector2 direction = Vector2.Normalize(new Vector2(speedX, speedY));
            direction *= startRadius;

            for (int i = 0; i < projectileCount; i++)
            {

                float angleStep = (float)(i - (projectileCount - 1)) / 12f;
                Vector2 firePosition = direction.RotatedBy(angleStep * spread + 2);

                Projectile.NewProjectile(position.X + firePosition.X, position.Y + firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
            }
            for (int i = 0; i < projectileCount; i++)
            {

                float angleStep = (float)(i - (projectileCount - 1)) / 12f;
                Vector2 firePosition = direction.RotatedBy(angleStep * spread - 2);

                Projectile.NewProjectile(position.X + firePosition.X, position.Y + firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
            }

What I've done here is I've added (+2) to the first instance of the angleStep * spread calculation, and then subtracted (-2) from the second instance. This places two projectiles even spaced apart. I'm even using a third instance (without any for loop) of a completely different projectile that shoots in the center ;-)

Again, if there's a much better (cleaner, shorter, more elegant) way of doing this, just let me know :)

I've been able to take what you gave me and modify it for my own special case, so I'd say that's a win!
 
Last edited:
This only seems to work well with three projectiles. Using two, or even four projectiles, I can't get the spread to center properly.
It should actually work for any number of projectiles, and I think this is why it doesn't in your case:
C#:
float angleStep = (float)(i - (projectileCount - 1)) / 12f;
That should actually be a division by 2, not 12. If 2, it should work for any amount of projectiles (very basically what it does is evenly spread half of the projectiles above the center, half of it below and, in case of an odd number, one in the middle. It's the one number you can't tweak. :p
 
It should actually work for any number of projectiles, and I think this is why it doesn't in your case:
C#:
float angleStep = (float)(i - (projectileCount - 1)) / 12f;
That should actually be a division by 2, not 12. If 2, it should work for any amount of projectiles (very basically what it does is evenly spread half of the projectiles above the center, half of it below and, in case of an odd number, one in the middle. It's the one number you can't tweak. :p


I don't actually remember changing those numbers... That's odd..

Anyhow, I can't get this to work with just two projectiles.

Here's the code unmodified. They both shoot from the same spot. Could this possibly have to do with the size of the bow?


C#:
            int projectileCount = 2;
            float startRadius = 15f; // This is how far from the center the bullets will start.
            float spread = (float)Math.PI / 18f; // The angle between bullets in radians (PI = 180 degrees).

            Vector2 direction = Vector2.Normalize(new Vector2(speedX, speedY));
            direction *= startRadius;

            for (int i = 0; i < projectileCount; i++)
            {

                float angleStep = (float)(i - (projectileCount - 1)) / 2f;
                Vector2 firePosition = direction.RotatedBy(angleStep * spread);

                Projectile.NewProjectile(position.X + firePosition.X, position.Y + firePosition.Y, speedX, speedY, mod.ProjectileType<ModProjectile>(), damage, knockBack, player.whoAmI);
            }

The only way I could get this to "sort of" work was by changing the spread float number to something much, much lower, like 0.4f. At 18f, they all shoot from the same exact spot, no matter how many projectiles I specify. Changing the startRadius to something much higher also seems to get them to spread, but then they start to appear in a spot far away from the weapon.

A startRadius of 15f and a spread of 0.4f is the best I could do, and that doesn't work well with just two projectiles. The only way two projectiles work well is by increasing that startRadius to something drastic. I should probably specify that I can actually get them to have a spread with just two projectiles, but adjusting the numbers takes awhile to get them to be "even" on top/bottom. I can get them close, but not exactly evenly spread.

I've changed my code back to two instead of twelve (still no idea how that happened - I think that was just a typo), and I'm using two instances of the same code without the for loop, and again, just added or subtracted two from the angleStep * spread calculation. That gives me a nice, very even spread of two projectiles.

I'm not exactly sure what's going on with the above code, but I have a hunch that it might have to do with the size of my weapon. It's a larger than normal bow, so perhaps that's what's causing this? Either that, or there's something else going on in my code.
 
Last edited:
Back
Top Bottom