tModLoader Projectile trail drawing incorrectly

So, I made a slightly modified version of the Example Custom Swing Sword that only does spins, and after looking at other peoples' code on how they drew trails, I implemented a trail into my sword as well. Only problem being, it draws incorrectly.
SwingBug.gif


Expected Behaviour: Trail composed of multiple duplicate textures is left behind as the projectile moves, gradually decreasing in alpha to create a "fading ribbon" appearance.

Actual Behaviour: Trail clusters in two places, doesnt follow the projectile, and the clusters hog so many of the available trail textures that later trail draws look very choppy.

C#:
using FranciumCalamityWeapons.Content.Particles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ReLogic.Content;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.Audio;
using Terraria.DataStructures;
using Terraria.GameContent;
using Terraria.GameContent.Drawing;
using Terraria.Graphics.Shaders;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.Social.Base;

namespace FranciumCalamityWeapons.Content.Projectiles
{
    public class OverlordSwing : ModProjectile
    {
        private const float SWINGRANGE = 1.67f * (float)Math.PI;
        private const float SPINRANGE = 4.5f * (float)Math.PI;
        private const float WINDUP = 0.15f;
        private const float UNWIND = 0.4f;
        private const float SPINTIME = 2.0f;

        private enum AttackType
        {
            Spin, //This has no purpose in the code anymore beyond its usage in CurrentAttack, since its the only attack type, theres no need to check it later in the code.
        }

        public List<float> TrailIndex = new List<float>();
        public List<Vector2> TrailIndexPos = new List<Vector2>();

        private enum AttackStage
        {
            Prepare,
            Execute,
            Unwind
        }

        private AttackType CurrentAttack {
            get => (AttackType)Projectile.ai[0];
            set => Projectile.ai[0] = (float)value;
        }

        private AttackStage CurrentStage {
            get => (AttackStage)Projectile.localAI[0];
            set {
                Projectile.localAI[0] = (float)value;
                Timer = 0;
            }
        }

        private ref float InitialAngle => ref Projectile.ai[1];
        private ref float Timer => ref Projectile.ai[2];
        private ref float Progress => ref Projectile.localAI[1];
        private ref float Size => ref Projectile.localAI[2];

        private float prepTime => 12f / Owner.GetTotalAttackSpeed(Projectile.DamageType);
        private float execTime => 12f / Owner.GetTotalAttackSpeed(Projectile.DamageType);
        private float hideTime => 12f / Owner.GetTotalAttackSpeed(Projectile.DamageType);

        public override string Texture => "FranciumCalamityWeapons/Content/Projectiles/OverlordSwing";
        private Player Owner => Main.player[Projectile.owner];

        public override void SetStaticDefaults()
        {
            ProjectileID.Sets.HeldProjDoesNotUsePlayerGfxOffY[Type] = true;
            ProjectileID.Sets.TrailCacheLength[Type] = 15;
            ProjectileID.Sets.TrailingMode[Type] = 2;
        }

        public override void SetDefaults() {
            Projectile.width = 174;
            Projectile.height = 174;
            Projectile.friendly = true;
            Projectile.timeLeft = 10000;
            Projectile.penetrate = -1;
            Projectile.tileCollide = false;
            Projectile.usesLocalNPCImmunity = true;
            Projectile.localNPCHitCooldown = -1;
            Projectile.ownerHitCheck = true;
            Projectile.DamageType = DamageClass.Melee;
        }

        public override void OnSpawn(IEntitySource source) {
            Projectile.spriteDirection = Main.MouseWorld.X > Owner.MountedCenter.X ? 1 : -1;
            float targetAngle = (Main.MouseWorld - Owner.MountedCenter).ToRotation();

            
            InitialAngle = (float)(-Math.PI / 2 - Math.PI * 1 / 3 * Projectile.spriteDirection);
            
        }

        public override void SendExtraAI(BinaryWriter writer) {
            writer.Write((sbyte)Projectile.spriteDirection);
        }

        public override void ReceiveExtraAI(BinaryReader reader) {
            Projectile.spriteDirection = reader.ReadSByte();
        }

        public override void AI() {
            TrailIndex.Add(Projectile.rotation);
            TrailIndexPos.Add(Projectile.position);

            if (TrailIndex.Count > 260)
            {
                TrailIndex.RemoveAt(0);
            }
            if (TrailIndexPos.Count > 260)
            {
                TrailIndexPos.RemoveAt(0);
            }

            Owner.itemAnimation = 2;
            Owner.itemTime = 2;

            if (!Owner.active || Owner.dead || Owner.noItems || Owner.CCed) {
                Projectile.Kill();
                return;
            }

            switch (CurrentStage) {
                case AttackStage.Prepare:
                    PrepareStrike();
                    break;
                case AttackStage.Execute:
                    ExecuteStrike();
                    break;
                default:
                    UnwindStrike();
                    break;
            }

            SetSwordPosition();
            Timer++;
        }

        public override bool PreDraw(ref Color lightColor) {

            Vector2 origin;
            float rotationOffset;
            SpriteEffects effects;

            if (Projectile.spriteDirection > 0) {
                origin = new Vector2(0, Projectile.height);
                rotationOffset = MathHelper.ToRadians(45f);
                effects = SpriteEffects.None;
            }
            else {
                origin = new Vector2(Projectile.width, Projectile.height);
                rotationOffset = MathHelper.ToRadians(135f);
                effects = SpriteEffects.FlipHorizontally;
            }

            Texture2D texture = TextureAssets.Projectile[Type].Value;

            Main.spriteBatch.Draw(texture, Projectile.Center - Main.screenPosition, default, lightColor * Projectile.Opacity, Projectile.rotation + rotationOffset, origin, Projectile.scale, effects, 0);

            return false;
        }

        public override void PostDraw(Color lightColor) {
            DrawTrail(lightColor);
        }

        private void DrawTrail(Color lightColor) {
            Texture2D Trailtexture = ModContent.Request<Texture2D>("FranciumCalamityWeapons/Content/Extras/HeroSwordTrail").Value;
            Vector2 drawOrigin = new Vector2(Projectile.width * 0.5f, Projectile.height * 0.5f);
            SpriteBatch Spritebatch = Main.spriteBatch;

            for (int k = 1; k < TrailIndexPos.Count; k++) {
                Vector2 startPos = TrailIndexPos[k - 1];
                Vector2 endPos = TrailIndexPos[k];

                int interpolationSteps = 36;
                for (int i = 0; i < interpolationSteps; i++) {
                    float t = i / (float)interpolationSteps;
                    Vector2 interpolatedPos = Vector2.Lerp(startPos, endPos, t);
                    float interpolatedRotation = MathHelper.Lerp(TrailIndex[k - 1], TrailIndex[k], t);
                    float alpha = 1f - ((float)k / TrailIndexPos.Count);
                    if (alpha < 0f) alpha = 0f;
                    Vector2 drawPos = (interpolatedPos - Main.screenPosition) + drawOrigin + new Vector2(0f, Projectile.gfxOffY);
                    Color color = Projectile.GetAlpha(lightColor) * alpha * ((TrailIndexPos.Count - k) / (float)TrailIndexPos.Count) * (1 - t);

                    Spritebatch.End();
                    Spritebatch.Begin(SpriteSortMode.Immediate, BlendState.Additive, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone, null, Main.GameViewMatrix.TransformationMatrix);
                    Main.EntitySpriteDraw(Trailtexture, drawPos, null, color * alpha, interpolatedRotation, drawOrigin, 0.2f * Projectile.scale, SpriteEffects.None, 0);
                    Spritebatch.End();
                    Spritebatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.AnisotropicClamp, DepthStencilState.None, RasterizerState.CullNone, null, Main.GameViewMatrix.TransformationMatrix);
                }
            }
        }

        public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
        {
            Vector2 start = Owner.MountedCenter;
            Vector2 end = start + Projectile.rotation.ToRotationVector2() * ((Projectile.Size.Length()) * Projectile.scale);
            float collisionPoint = 0f;
            return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), start, end, 15f * Projectile.scale, ref collisionPoint);
        }

        public override void CutTiles() {
            Vector2 start = Owner.MountedCenter;
            Vector2 end = start + Projectile.rotation.ToRotationVector2() * (Projectile.Size.Length() * Projectile.scale);
            Utils.PlotTileLine(start, end, 15 * Projectile.scale, DelegateMethods.CutTiles);
        }

        public override bool? CanDamage() {
            if (CurrentStage == AttackStage.Prepare)
                return false;
            return base.CanDamage();
        }

        public override void ModifyHitNPC(NPC target, ref NPC.HitModifiers modifiers) {
            modifiers.HitDirectionOverride = target.position.X > Owner.MountedCenter.X ? 1 : -1;
            modifiers.Knockback += 1;
        }

        public void SetSwordPosition() {
            Projectile.rotation = InitialAngle + Projectile.spriteDirection * Progress;

            Owner.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full, Projectile.rotation - MathHelper.ToRadians(90f));
            Vector2 armPosition = Owner.GetFrontHandPosition(Player.CompositeArmStretchAmount.Full, Projectile.rotation - (float)Math.PI / 2);

            armPosition.Y += Owner.gfxOffY;
            Projectile.Center = armPosition;
            Projectile.scale = Size * 1.2f * Owner.GetAdjustedItemScale(Owner.HeldItem);

            Owner.heldProj = Projectile.whoAmI;
        }

        private void PrepareStrike() {
            Progress = WINDUP * SWINGRANGE * (1f - Timer / prepTime);
            Size = MathHelper.SmoothStep(0, 1, Timer / prepTime);

            if (Timer >= prepTime) {
                SoundEngine.PlaySound(new SoundStyle("FranciumCalamityWeapons/Audio/Swing1"));
                CurrentStage = AttackStage.Execute;
            }
        }

        private void ExecuteStrike() {
            Player player = Main.player[Projectile.owner];

            Progress = MathHelper.SmoothStep(0, SPINRANGE, (1f - UNWIND / 2) * Timer / (execTime * SPINTIME));

            if (Timer == (int)(execTime * SPINTIME * 3 / 4)) {
                SoundEngine.PlaySound(new SoundStyle("FranciumCalamityWeapons/Audio/Swing1"));
                Projectile.ResetLocalNPCHitImmunity();
            }

            if (Timer >= execTime * SPINTIME) {
                CurrentStage = AttackStage.Unwind;
            }
            
        }

        private void UnwindStrike() {
            Progress = MathHelper.SmoothStep(0, SPINRANGE, (1f - UNWIND / 2) + UNWIND / 2 * Timer / (hideTime * SPINTIME / 2));
            Size = 1f - MathHelper.SmoothStep(0, 1, Timer / (hideTime * SPINTIME / 2));

            if (Timer >= hideTime * SPINTIME / 2) {
                Projectile.Kill();
            }
        }

        public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
        {
            Color IceColor = Color.SkyBlue;
            Color FireColor = Color.Pink;

            float lerpAmount = (float)(0.5 * (1 + Math.Sin(Main.GlobalTimeWrappedHourly * 2f * Math.PI)));
            Color entityhitcolor = Color.Lerp(IceColor, FireColor, lerpAmount);
            Player player = Main.LocalPlayer;
            player.GetModPlayer<ScreenshakePlayer>().screenshakeMagnitude = 9;
            player.GetModPlayer<ScreenshakePlayer>().screenshakeTimer = 24;
            Lighting.AddLight(target.Center, entityhitcolor.ToVector3() * 0.8f);
            SoundEngine.PlaySound(new SoundStyle("FranciumCalamityWeapons/Audio/GalaxySmasherSmash"));
            Vector2 Flamedirection = new Vector2((float)Math.Cos(MathHelper.ToRadians(90)), (float)Math.Sin(MathHelper.ToRadians(90)));
            Vector2 Frostdirection = new Vector2((float)Math.Cos(MathHelper.ToRadians(270)), (float)Math.Sin(MathHelper.ToRadians(270)));
            Projectile.NewProjectile(Entity.GetSource_OnHit(target), Projectile.Center, Flamedirection, ModContent.ProjectileType<CosmicStarPink>(), 3000, 8, Main.myPlayer);
            Projectile.NewProjectile(Entity.GetSource_OnHit(target), Projectile.Center, Frostdirection, ModContent.ProjectileType<CosmicStarBlue>(), 3000, 8, Main.myPlayer);
            if (damageDone < target.life)
            {
            Projectile.NewProjectile(Entity.GetSource_OnHit(target), target.Center, new Vector2(0, 0), ModContent.ProjectileType<BoomDrawEntity>(), 0, 0, Main.myPlayer);
            }
            if (hit.Crit)
            {
                if (ModLoader.TryGetMod("CalamityMod", out Mod calamityMod))
                {
                    if (calamityMod.TryFind("WhisperingDeath", out ModBuff WD))
                    {
                    target.AddBuff(WD.Type, 360);
                    }
                }
            }
        }
    }
}
 
I tried to re-create your sword, but when I did, I failed to get the issue you showed; the positioning of the trails were correct. Unfortunately, your projectile is very messy and makes no sense at all. There are only 2 issue that I can say, for sure, that are issues:
1. The first trail is added before the sword is positioned. This causes the first value to be at "zero" instead of the initial position of the sword. To fix this, move the part that adds to TrailIndex and TrailIndexPos to the end of AI(). The bottom right section of the trail is caused by this error.
2. The way you calculate the alpha of each part of the trail is bad. The way you have it, the alpha is squared and depends on the index of the record. The way you probably should have it would be to have the alpha be scaled based on how long that record has existed. You can find out how many frames old the record is in the DrawTrail() with:
C#:
int age = TrailIndexPos.Count - k;
Then, you can calculate the alpha based on age with:
C#:
float alpha = 2f/(age - t + 2);
If you would like it to decay slower, then you may increase the two constants (which are "2" in this example) to any value greater than one. The two numbers should be the same.
...
Also, for the top right of the trail, it looks to be correct except for being rotated +PI/4. If you subtract PI/4 to the rotations before they are added to TrailIndex, in addition to fixing the two other things above, then it should give you something that is close to "working".
 
Last edited:
Back
Top Bottom