tAPI [Tutorial] Custom Hitboxes and Object-Oriented Programming

blushiemagic

Retinazer
tModLoader
Has it ever annoyed you how all hitboxes are square when you want a circle projectile? Or how you need to create many separate projectiles in order to give the illusion of a trail? (I'm looking at you, Rainbow Gun...) This tutorial will give you some copy/paste code to fine-tune projectile hitboxes, teach a bit about object-oriented programming, and try to get you in the mindset of a programmer (although I don't know how successful that part will be). If you don't understand a lot of stuff, then you can just copy/paste the code I have here (although I'd appreciate credit for the complex hitboxes).

In my view, custom hitboxes can be divided into two categories: simple and complex. Simple hitboxes can fit within a rectangle and fill up most of the space: for example, circles. They only require addition collision checking after the rectangle collision check passes. Complex hitboxes are for weird shapes like trails or diagonal rectangles; if you try to fit them into a rectangle, there's a ton of empty space, so additional code must be made using the projectile's ai array to help with collision detection.

Simple Hitboxes

As explained earlier, just a little additional collision checking is needed for a simple hitbox once the initial rectangular check has passed. We can make it so that if our additional checks fail, the projectile cannot damage the target. Fortunately, tAPI provides us with some CanHit hooks that allow us to stop damage. So, if the projectile actually collides with the target, we can return null, and if the projectile does not collide, we can return false. Like this:
Code:
using System;
using Terraria;
using TAPI;

namespace ExampleMod.Projectiles {
public class SimpleHitbox : ModProjectile
{
    public override bool? CanHitPlayer(Player p)
    {
        return CanHit(p);
    }

    public override bool? CanHitPVP(Player p)
    {
        return CanHit(p);
    }

    public override bool? CanHitNPC(NPC npc)
    {
        return CanHit(npc);
    }

    public bool? CanHit(CodableEntity entity)
    {
        if(Collides(entity))
        {
            return false;
        }
        return null;
    }

    public virtual bool Collides(CodableEntity entity)
    {
        return true;
    }
}}
At this point, I should probably explain some things about programming. C# is (partially) an object-oriented programming language. (It's actually multi-paradigm, but this includes object-oriented. Don't worry if you don't understand this parentheses.) Object-oriented languages feature things called classes and objects. Think of an object as essentially anything that can remember things and/or do things. For example, in Terraria, players remember where they are and what items they have, and they can do things like move and attack. Now, think of classes as basically types of objects. So for example, the specific player you play can be thought of as an object, while players in general can be thought of as a class. Each class defines objects with the specific things they can remember and do.

Now here's the fun part: classes can "extend" or "override" each other. Notice this line near the top:
Code:
public class SimpleHitbox : ModProjectile
What this does is let the computer know that there is a class called SimpleHitbox. Moreover, it tells the computer that the SimpleHitbox class "extends" or "overrides" the ModProjectile class. The class that is extending (In this case, SimpleHitBox) is known as the subclass, while the class that is extended (in this case ModProjectile) is known as the superclass. The act of extending / overriding is also known as inheritance. The cool thing about inheritance is, the subclass gets everything that the superclass gets. But what exactly does the subclass get? As an example, let's look at this line in the ModProjectile class:
Code:
public virtual bool? CanHitPlayer(Player p)
This line essentially tells the computer that the ModProjectile class has a "method" or a "function" called CanHitPlayer. It also tells the computer that the method "returns" a nullable Boolean value (the question mark means it's nullable, and Boolean means true of false) and that the method needs an object of the class Player as an input. Methods / functions are essentially the things that objects can do. When a method "returns" something, that means the method is essentially calculating the result of what it does and tells the result to the computer. For ModProjectile.CanHitPlayer, it's basically calculating whether or not it can hit the player. For ModProjectile, this always returns null, which in the case of tAPI means it can hit the player. Not really useful for our purposes, which is why we "override" the method in SimpleHitbox.
Code:
public override bool? CanHitPlayer(Player p)
Notice the "virtual" in the ModProjectile class (superclass) and the "override" in the SimpleHitbox class (subclass). Recall that the subclass gets everything that the superclass gets. But what if, instead of inheriting everything, you want to inherit some stuff then replace the rest? This is where the "virtual" and "override" come in. In C#, placing virtual in a method declaration basically allows subclasses to override that method. When you override a method, you're essentially replacing it with something different for that subclass only. So while a ModProjectile can always hit a player, a SimpleHitbox can only hit a player when the player collides with whatever the actual custom hitbox is. (Note that CanHitPlayer only ever gets called if the projectile's rectangular hitbox collides with the player. A call basically means actually doing whatever's inside the method.)

But what is the use of this? Instead of having to go through the bother of overriding things, can't you just create an entirely different class with the same things? The answer is, having a superclass allows for generalization. A single superclass can have multiple subclasses. By now you've probably had to make multiple subclasses of ModProjectile to do different things for different projectiles. The computer can store subclass objects as instances of the superclass; in other words, the computer can act like a SimpleHitbox object is in fact a ModProjectile object. When a call is made to a method of a ModProjectile object, and the object is actually a SimpleHitbox, the SimpleHitbox's method gets called instead. So think of it this way: a SimpleHitbox disguises as a ModProjectile. The computer tells what it thinks is a ModProjectile to do CanHitPlayer. The SimpleHitbox then uses its own CanHitPlayer instead. This makes things rather convenient.
Another reason to override things is, you don't need to copy/paste code as much. :p

So why am I explaining all this? Look at this line in SimpleHitbox:
Code:
public virtual bool Collides(CodableEntity entity)
CodableEntity is essentially anything that can exist in the Terraria world, such as players, NPCs, or projectiles.
Now here's a quiz: why might we be declaring this virtual method in SimpleHitbox?

The most common type of hitbox that people want is the circle. Circles happen to fit rather nicely inside a square; similarly, ellipses happen to fit rather nicely inside a rectangle. For those that don't know, an ellipse is basically an oval. A circle is just a special type of ellipse (like how a square is a type of rectangle), so we will just implement the hitbox for ellipses in general.
First of all, it will help if you know the formula for an ellipse:
Code:
(x ^ 2) / (a ^ 2) + (y ^ 2) / (b ^ 2) = 1
Where x and y are the coordinates of a point on the ellipse's edge, a is half the width of the ellipse, and b is half the height the ellipse. (a and b are comparable to the radius r of a circle. In fact, if a = b, then the ellipse is a circle with r = a = b.) So for any values of x and y you plug into the equation, the point (x, y) is on the edge of the ellipse if it results in 1. However, if you get a result less than 1, then the point is inside the ellipse. Similarly, if the result is greater than 1, the point is outside the ellipse. (Note that this equation assumes that the center of the ellipse is at the point (0, 0).) So now we know how to tell whether or not a point collides with an ellipse. But entities are not points; they are rectangles. So how do we tell if a rectangle collides with an ellipse?

The easiest way would be to choose the point in the rectangle closest to the center of the ellipse. How do we do this? Note that, since the equation assumes that the ellipse is at the center of the graph, x and y will represent the distances from the ellipse's center, rather than the actual position of the point we choose. Notice that if we decrease x, the result becomes smaller, The same thing happens if we decrease y. (The square of a real number is never negative.) So, sliding a point closer in just one direction can make it closer to the ellipse's center. The smallest possible values of x squared and y squared are 0, so first we must see if such an offset exists in the rectangle. If it does, then we use that coordinate; otherwise, we slide the point as much as possible so we use a corner of the rectangle.

Code:
using System;
using Microsoft.Xna.Framework;

namespace ExampleMod.Projectiles {
public class Ellipse : SimpleHitbox
{
    public override bool Collides(CodableEntity entity)
    {
        return Collides(projectile.position, projectile.Size, entity.position, entity.Size);
    }

    public bool Collides(Vector2 ellipsePos, Vector2 ellipseDim, Vector2 boxPos, Vector2 boxDim)
    {
        Vector2 ellipseCenter = ellipsePos + 0.5f * ellipseDim;
        float x = 0f; //ellipse center
        float y = 0f; //ellipse center
        if(boxPos.X > ellipseCenter.X)
        {
            x = boxPos.X - ellipseCenter.X; //left corner
        }
        else if(boxPos.X + boxDim.X < ellipseCenter.X)
        {
            x = boxPos.X + boxDim.X - ellipseCenter.X; //right corner
        }
        if(boxPos.Y > ellipseCenter.Y)
        {
            y = boxPos.Y - ellipseCenter.Y; //top corner
        }
        else if(boxPos.Y + boxDim.Y < ellipseCenter.Y)
        {
            y = boxPos.Y + boxDim.Y - ellipseCenter.Y; //bottom corner
        }
        float a = ellipseDim.X / 2f;
        float b = ellipseDim.Y / 2f;
        return (x * x) / (a * a) + (y * y) / (b * b) < 1; //point collision detection
    }
}}
If you just want the custom hitbox, you can copy that code and just be done. Make sure that in your .cs file, you either override Ellipse instead of ModProjectile, or you use the "code" property in the .json file to use the Ellipse class, like this:
Code:
"code": "ExampleMod.Projectiles.Ellipse"
For those that are here to learn about programming, I will explain about overloaded methods and local variables now. First of all, notice how the Ellipse has two methods, and they are both named Collide. Also notice that only one of them has "override" in them. It turns out that methods can have the same name and still count as different methods if they have different parameters. Parameters are the things in the parentheses; the inputs that the method requires. The computer is able to tell which method to use based on the parameters you give it. For example, the first Collides method only has a CodableEntity parameter, which means it is the same as the one from SimpleHitbox and must use override. The second Collides method takes in 4 Vector2 objects as parameters, which makes it different from the first one. For this reason, methods are referred to by both their names and their parameters. For example, Collides(CodableEntity) and Collides(Vector2, Vector2, Vector2, Vector2) are different methods. When different methods have the same name, they are called overloaded methods. The first Collides makes a call to the second collides, and the computer is able to tell since four Vector2 objects are given as the inputs.
Now for local variables. In order for an object to do things, sometimes it must remember things. There is where local variables come in. Local variables are things like this:
Code:
Vector2 ellipseCenter = ellipsePos + 0.5f * ellipseDim;
float a = ellipseDim.X / 2f;
Essentially, what a local variable does is allow you to store a value, such as a Vector2 or a float, under a name, then use that name to refer to the value. You can change the value under the name later on if you want, or use it to calculate more complicated stuff. Local variables only exist within the method they are declared (or if you know what blocks are, only within the blocks they are declared), which is why they are called local. There's not much to say about them since they're relatively simple; just names to store values in. You can create them by typing the type of data you want to store, followed by the name, then assigning a value with the equals symbol. Their main uses are to shorten lines of code and to reduce the computation the computer must perform.

Complex Hitboxes

When you use the code above for ellipses, you may notice that the projectile still cuts grass, etc., as if it were a square. There are two solutions if you don't want this to happen: one would be to set "hurtsTiles" to false in the .json file. The other solution, would be to use a complex hitbox. I won't go over complex hitboxes for ellipses, since in my opinion it's not worth the effort. However, some hitboxes you might want cannot be accurately represented by a rectangle, and must use custom hitboxes. Word of warning: you must have access to Terraria's source code for complex hitboxes, because there are some parts you will need to copy over.

With complex hitboxes, the goal is to not rely on the game's rectangle collision detection. Instead, we'll be using our own collision detection, and do the actual damage in the PostAI method. This means that we'll also have to make sure that the projectile's actual position and size stay within the bounds of the projectile's custom hitbox, so that enemies don't get hurt by invisible things in the game's default damaging methods. The ComplexHitbox class provides a Collides method and an AttackDirection method that can both be overriden. The Collides method is what the subclasses will use to determine their custom hitboxes; the AttackDirection method will determine which direction to knock back enemies.
Code:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using TAPI;

namespace ExampleMod.Projectiles {
public class ComplexHitbox : ModProjectile
{
    public override void PostAI()
    {
        Damage();
    }

    public void Damage()
    {
        if (projectile.owner == Main.myPlayer && projectile.damage > 0)
        {
            Player player = Main.player[projectile.owner];
            for (int k = 0; k < 200; k++)
            {
                NPC curNPC = Main.npc[k];
                if (curNPC.active && !curNPC.dontTakeDamage && (((!curNPC.friendly || (curNPC.type == 22 && projectile.owner < 255 && player.killGuide) || (curNPC.type == 54 && projectile.owner < 255 && player.killClothier)) && projectile.friendly) || (curNPC.friendly && projectile.hostile)) && (projectile.owner < 0 || curNPC.immune[projectile.owner] == 0 || projectile.maxPenetrate == 1) && Collides(curNPC))
                {
                    bool? flag = projectile.CanHitNPC(curNPC);
                    if(!flag.HasValue)
                    {
                        flag = player.ProjectileCanHitNPC(projectile, curNPC);
                    }
                    if((!flag.HasValue || flag.Value) && ((flag.HasValue && flag.Value) || curNPC.noTileCollide || !projectile.ownerHitCheck || Collision.CanHit(player.position, player.width, player.height, curNPC.position, curNPC.width, curNPC.height)))
                    {
                        if(!DamageNPC(curNPC))
                        {
                            break;
                        }
                    }
                }
            }
            if (Main.localPlayer.hostile)
            {
                for (int l = 0; l < 255; l++)
                {
                    Player subPlayer = Main.player[l];
                    if (l != projectile.owner && subPlayer.active && !subPlayer.dead && !subPlayer.immune && subPlayer.hostile && projectile.playerImmune[l] <= 0 && (Main.localPlayer.team == 0 || Main.localPlayer.team != subPlayer.team) && Collides(subPlayer))
                    {
                        bool? flag = projectile.CanHitPVP(subPlayer);
                        if(!flag.HasValue)
                        {
                            flag = player.ProjectileCanHitPVP(projectile, subPlayer);
                        }
                        if((!flag.HasValue || flag.Value) && ((flag.HasValue && flag.Value) || !projectile.ownerHitCheck || Collision.CanHit(player.position, player.width, player.height, subPlayer.position, subPlayer.width, subPlayer.height)))
                        {
                            if(!DamagePVP(subPlayer))
                            {
                                break;
                            }
                        }
                    }
                }
            }
        }
        if (Main.netMode != 2 && projectile.hostile && Main.myPlayer < 255 && projectile.damage > 0 && Main.localPlayer.active && !Main.localPlayer.dead && !Main.localPlayer.immune && Collides(Main.localPlayer))
        {
            bool? flag = projectile.CanHitPlayer(Main.localPlayer);
            if (flag.HasValue && !flag.Value)
            {
                return;
            }
            DamageMyPlayer();
        }
    }

    public bool DamageNPC(NPC curNPC)
    {
        Player player = Main.player[projectile.owner];
        projectile.direction = AttackDirection(curNPC);
        //Copy over Terraria's code for damaging NPCs here. It is in Projectile.Damage().
        //The specific segment of code starts with bool crit = false; and ends with this.numHits++;
        //You will have to rename some local variables in order to get everything to work.
        //You will have to replace break; with return false;
        //If you get too confused, I have the full ComplexHitbox class in my mod under the name CustomHitbox.
        return true;
    }

    public bool DamagePVP(Player subPlayer)
    {
        Player player = Main.player[projectile.owner];
        projectile.direction = AttackDirection(subPlayer);
        //Do the same thing as with NPCs, except with the PVP part
        return true;
    }

    public void DamageMyPlayer()
    {
        //the code for this is simple, so I'll just include it here
        int num13 = AttackDirection(Main.localPlayer);
        int num14 = projectile.damage;
        if (Main.localPlayer.resistCold && projectile.coldDamage)
        {
            num14 = (int)((float)num14 * 0.7f);
        }
        int num15 = projectile.npcCritChance;
        bool crit2 = Main.rand.Next(1, 101) <= num15;
        float critMult = projectile.npcCritMult;
        Main.localPlayer.ProjectileDamagePlayer(projectile, num13, ref num14, ref crit2, ref critMult);
        projectile.DamagePlayer(Main.localPlayer, num13, ref num14, ref crit2, ref critMult);
        num14 = Main.DamageVar((float)num14, projectile.player);
        int dmgDealt = (int)Main.localPlayer.Hurt(num14 * 2, num13, false, false, Lang.deathMsg(-1, -1, projectile.whoAmI, -1), crit2, critMult);
        Main.localPlayer.ProjectileDealtPlayer(projectile, num13, dmgDealt, crit2);
        projectile.DealtPlayer(Main.localPlayer, num13, dmgDealt, crit2);
    }

    public virtual bool Collides(CodableEntity entity)
    {
        return projectile.Hitbox.Intersects(entity.Hitbox);
    }

    public virtual int AttackDirection(CodableEntity target)
    {
        return projectile.direction;
    }
}}

Suppose you want a projectile to leave a damaging trail behind it as it moves, or you want a weapon to behave like the rainbow gun. The rainbow gun projectile actually consists of many projectiles touching each other. This works fine since the rainbow is wide enough for each individual projectile to be large, resulting in relatively few projectiles needed to make the actual rainbow. However, what if you want an even longer trail, or a skinnier trail? The game has a limit of 1000 projectiles; if you have several trails at once, it could quickly reach this limit. (This actually happened to me when I was designing an arrow projectile for my own mod.) In order to get around this, we'll need to fit all these projectiles into just a single projectile, with a custom hitbox. Since trails aren't really well-defined, rectangles aren't a good representation of it, so we'll need a complex hitbox.

Our motivation to create trail hitboxes gives us information on how to make them; they are essentially a chain of smaller projectiles. So a simple way to implement this would be to keep track of this big list of positions that represent where the individual parts of the trail are. But think of this: every time you want to check if something collides with the trail, you will need to check if it collides with every single position in the trail. Certainly this does not seem optimal! If only there were a way to make it so that we didn't need to check collision with every single position...

One of the greatest advantages of rectangle collisions is that they are fast. If you want to detect whether something collides with a rectangle, all you need to do is check to see if it's inside the boundaries, which are conveniently aligned with the x and y axes. Maybe there's a way to group parts of the trail into rectangles, so that we can check the rectangle collisions first to see if it's worth it to check the individual positions? Think about the trail's shape, possibly twisting and turning in unexpected ways. However, there is one thing we can count on: either it will keep going in the same general diagonal direction, or it will eventually change directions to one of the other three diagonals. (Here by diagonal direction I mean up-left, up-right, down-left, or down-right.) Now consider a single "stretch" of the trail before it changes directions. This stretch can fit in a rectangle and cover two opposite corners. So we can check for a collision with the rectangle for each stretch of the trail, and for each stretch with a colliding rectangle, we can check for a collision with the individual positions.

There is one more major problem: how to create the shape of the trail. Two things must be done: we must be able to add more positions to the trail as it travels, and we must be able to remove old positions from the back of the trail. Every time we add or remove a position, we must determine whether or not there is a new stretch, and we must update the rectangles for all the stretches. Here is the code for trail hitboxes; as you read the code, keep the explanations above in mind.
Code:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using TAPI;

namespace ExampleMod.Projectiles {
public class TrailHitbox : CustomHitbox
{
    public Stretch start;
    private Vector2 topLeft;
    private Vector2 bottomRight;

    public int duration
    {
        get
        {
            return (int)projectile.ai[0];
        }
        set
        {
            projectile.ai[0] = value;
        }
    }

    public int age
    {
        get
        {
            return (int)projectile.ai[1];
        }
        set
        {
            projectile.ai[1] = value;
        }
    }

    public void Add(Vector2 pos)
    {
        int expires = age + duration;
        Vector2 halfSize = projectile.Size / 2f;
        if(start == null)
        {
            start = new Stretch(pos, expires);
            topLeft = pos - halfSize;
            bottomRight = pos + halfSize;
        }
        else
        {
            start.Add(pos, expires);
            if(pos.X - halfSize.X < topLeft.X)
            {
                topLeft.X = pos.X - halfSize.X;
            }
            else if(pos.X + halfSize.X > bottomRight.X)
            {
                bottomRight.X = pos.X + halfSize.X;
            }
            if(pos.Y - halfSize.Y < topLeft.Y)
            {
                topLeft.Y = pos.Y - halfSize.Y;
            }
            else if(pos.Y + halfSize.Y > bottomRight.Y)
            {
                bottomRight.Y = pos.Y + halfSize.Y;
            }
        }
        projectile.Center = pos;
        PostAdd(pos);
    }

    public virtual void PostAdd(Vector2 pos) {}

    public bool Remove()
    {
        if(start.Remove())
        {
            start = start.next;
        }
        return start == null;
    }

    public override void AI()
    {
        age++;
        if(start == null)
        {
            Add(projectile.Center);
        }
        CustomBehavior();
        while(start.start.expires <= age)
        {
            if(Remove())
            {
                projectile.active = false;
                return;
            }
        }
        projectile.timeLeft = 2;
    }

    public virtual void CustomBehavior() {}

    public override bool Collides(CodableEntity entity)
    {
        return Collides(entity.position, entity.position + entity.Size);
    }

    public bool Collides(Vector2 corner1, Vector2 corner2)
    {
        if(start == null || corner2.X < topLeft.X || corner2.Y < topLeft.Y || corner1.X > bottomRight.X || corner1.Y > bottomRight.Y)
        {
            return false;
        }
        Stretch current = start;
        Vector2 halfSize = projectile.Size / 2f;
        while(current != null)
        {
            Vector2 stretchStart = current.start.position;
            Vector2 stretchEnd = current.end.position;
            if(!(corner2.X < stretchStart.X && corner2.X < stretchEnd.X) && !(corner1.X > stretchStart.X && corner1.X > stretchEnd.X) && !(corner2.Y < stretchStart.Y && corner2.Y < stretchEnd.Y) && !(corner1.Y > stretchStart.Y && corner1.Y > stretchEnd.Y))
            {
                Spot spot = current.start;
                while(spot != null)
                {
                    Vector2 pos = spot.position;
                    if(pos.X - halfSize.X <= corner2.X && pos.X + halfSize.X >= corner1.X && pos.Y - halfSize.Y <= corner2.Y && pos.Y + halfSize.Y >= corner1.Y)
                    {
                        return true;
                    }
                    spot = spot.next;
                }
            }
            current = current.next;
        }
        return false;
    }

    public override int AttackDirection(CodableEntity target)
    {
        return -target.direction;
    }

    public virtual float GetAlpha(float timeLeft)
    {
        return 1f;
    }

    public override bool PreDraw(SpriteBatch sb)
    {
        Stretch current = start;
        while(current != null)
        {
            Spot spot = current.start;
            while(spot != null)
            {
                float timeLeft = (float)(spot.expires - age) / (float)duration;
                Color color = Lighting.GetColor((int)(spot.position.X / 16f), (int)(spot.position.Y / 16f));
                sb.Draw(Main.projectileTexture[projectile.type], spot.position - Main.screenPosition, null, color * GetAlpha(timeLeft), 0f, projectile.Size / 2f, 1f, SpriteEffects.None, 0f);
                CreateDust(spot.position);
                spot = spot.next;
            }
            current = current.next;
        }
        return false;
    }

    public virtual void CreateDust(Vector2 pos) {}

    public class Stretch
    {
        public Spot start;
        public Spot end;
        public Stretch next;
        public Stretch(Vector2 pos, int exp)
        {
            start = new Spot(pos, exp);
            end = start;
        }

        public void Add(Vector2 pos, int exp)
        {
            Stretch current = this;
            while(current.next != null)
            {
                current = current.next;
            }
            Vector2 direction = current.end.position - current.start.position;
            Vector2 nextDir = pos - current.end.position;
            if(direction.X * nextDir.X < 0f || direction.Y * nextDir.Y < 0f)
            {
                current.next = new Stretch(pos, exp);
            }
            else
            {
                current.end = current.end.Add(pos, exp);
            }
        }

        public bool Remove()
        {
            start = start.next;
            return start == null;
        }
    }

    public class Spot
    {
        public Vector2 position;
        public Spot next;
        public int expires;
        public Spot(Vector2 pos, int exp)
        {
            position = pos;
            expires = exp;
        }

        public Spot Add(Vector2 pos, int exp)
        {
            Spot end = this;
            while(end.next != null)
            {
                end = end.next;
            }
            end.next = new Spot(pos, exp);
            return end.next;
        }
    }
}}
To use this hitbox, either use the "code" property of your projectile (as described with simple hitboxes), or override the TrailHitbox class and perhaps override the virtual methods.

Take a look at these lines near the beginning of the code:
Code:
    public Stretch start;
    private Vector2 topLeft;
    private Vector2 bottomRight;
Notice that these look similar to local variables, except they are outside methods and have public / private before them. You don't need to worry about public / private that much; public just means you can use that variable from another class (for example you can change Player.position from your own classes). Recall that objects need to remember things; and it's not just enough for them to remember intermediate results for calculations. This is where fields come in. They allow the object to permanently "remember" things, useful for stuff like a projectile's position or size. In fact, while making or looking over code, you will notice that you need to use a period between a lot of stuff (such as player.position or object.property). Let us look at the example of "object.property". This means that you are accessing the field called "property" that is remembered by the "object".

Have you ever wished that you have a projectile with the shape of a sloped line? It would essentially behave like a diagonal / rotated rectangle, a slanted line with a width. A line extending from an origin point is more often needed than a diagonal rectangle, and since they're actually the same thing we'll implement diagonal hitboxes as slanted lines.

There are four things the line must keep track of: the origin, the length, the width, and the angle of rotation. Fortunately, we can already use the projectile's position to store the origin. While it is an option to use the projectile's rotation field to store the angle, I don't really like this since Projectile.rotation was made for aesthetic purposes. So, we'll have to store the angle in an ai array. We do not have the option to use Projectile.width and Projectile.height for the length and width, because the game uses these to keep track of the default rectangle hitboxes; in fact, to avoid any "invisible" collisions, we'll have to make sure the width and height are always kept at 1 (by using the .json file).

The thing we'll need to worry about most is the collision. The hitbox is shaped like a rectangle; maybe there's a way to keep collision detection simple as if it were a straight rectangle? Think about normal rectangle collision: let's say there are two rectangles. We must make sure the first one's bottom is below the second one's top, the first one's top is above the second one's bottom, the first one's left is to the left of the second one's right, and the first one's right is to the right of the second one's left. Except with diagonal rectangles, we don't have a top, left, bottom, and right. All we have is a top-left, top-right, bottom-left, and bottom-right... wait a minute...

We were comparing opposites with normal rectangle collision. Perhaps with collision detection between a diagonal rectangle and a straight rectangle, we could compare the sides of the diagonal rectangle with the corners of the normal one? The normal rectangle's bottom-right corner must be below the top-left side, the top-right corner must be above the bottom-left side, the top-left corner must be above the bottom-right side, and the bottom-left corner must be below the top-right side. Try to visualize this (or draw it if you can't).

Note: For this piece of code, the projectile's image must be pointing to the right. This image will be repeated along the length of the hitbox to fill up any empty space. You must also use ModNet to sync the angle between server and clients; my server syncing (custom bosses) tutorial has more information on that.
Code:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using TAPI;

namespace ExampleMod.Projectiles {
public class DiagonalHitbox : ComplexHitbox
{
    public Vector2 origin
    {
        get
        {
            return projectile.position;
        }
        set
        {
            projectile.position = value;
        }
    }
    public float width
    {
        get
        {
            return projectile.ai[0];
        }
        set
        {
            projectile.ai[0] = value;
        }
    }
    public float length
    {
        get
        {
            return projectile.ai[1];
        }
        set
        {
            projectile.ai[1] = value;
        }
    }
    public float angle
    {
        get
        {
            return projectile.localAI[0];
        }
        set
        {
            projectile.localAI[0] = value;
        }
    }
    private Texture2D tip = null;
    public DiagonalHitbox(Texture2D t)
    {
        tip = t;
    }

    public DiagonalHitbox() {}

    public override bool Collides(CodableEntity entity)
    {
        while(angle < 0f)
        {
            angle += 2f * (float)Math.PI;
        }
        angle %= 2f * (float)Math.PI;
        if(angle % ((float)Math.PI / 2f) == 0f)
        {
            float top = origin.Y;
            float left = origin.X;
            float rectWidth = length;
            float rectHeight = length;
            if(angle % (float)Math.PI == 0f)
            {
                top -= width / 2f;
                rectHeight = width;
            }
            else
            {
                left -= width / 2f;
                rectWidth = width;
            }
            if(angle == (float)Math.PI)
            {
                left -= length;
            }
            if(angle == (float)Math.PI * 1.5f)
            {
                top -= length;
            }
            return new Rectangle((int)left, (int)top, (int)rectWidth, (int)rectHeight).Intersects(entity.Hitbox);
        }
        float pAngle = angle + (float)Math.PI / 2f;
        Vector2 direction = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
        Vector2 normal = new Vector2((float)Math.Cos(pAngle), (float)Math.Sin(pAngle));
        direction *= length;
        normal *= width / 2f;
        Line topLeft = new Line(origin, pAngle);
        Line topRight = new Line(origin - normal, angle);
        Line bottomLeft = new Line(origin + normal, angle);
        Line bottomRight = new Line(origin + direction, pAngle);
        for(float k = (float)Math.PI / 2f; k < angle; k += (float)Math.PI / 2f)
        {
            Line temp = topLeft;
            topLeft = bottomLeft;
            bottomLeft = bottomRight;
            bottomRight = topRight;
            topRight = temp;
        }
        Vector2 entTopLeft = entity.position;
        Vector2 entTopRight = entity.position + new Vector2(entity.width, 0f);
        Vector2 entBottomLeft = entity.position + new Vector2(0f, entity.height);
        Vector2 entBottomRight = entity.position + entity.Size;
        return topLeft.f(entBottomRight.X) < entBottomRight.Y && topRight.f(entBottomLeft.X) < entBottomLeft.Y && bottomLeft.f(entTopRight.X) > entTopRight.Y && bottomRight.f(entTopLeft.X) > entTopLeft.Y;
    }

    public override int AttackDirection(CodableEntity target)
    {
        angle %= 2f * (float)Math.PI;
        if(angle % (float)Math.PI == 0f)
        {
            return -entity.direction;
        }
        if(angle % (float)Math.PI / 2f == 0f)
        {
            return target.Center.X < origin.X ? -1 : 1;
        }
        float yOffset = target.Center.Y - origin.Y;
        float x = origin.X + yOffset / (float)Math.Tan(angle);
        return target.Center.X < x ? -1 : 1;
    }

    public override bool PreDraw(SpriteBatch sb)
    {
        Texture2D unit = Main.projectileTexture[projectile.type];
        int unitLength = unit.Width;
        int numUnits = (int)Math.Ceiling(length / unitLength);
        float increment = 0f;
        if(numUnits > 1)
        {
            increment = (length - unitLength) / (numUnits - 1);
        }
        Vector2 direction = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
        SpriteEffects effects = SpriteEffects.None;
        if(projectile.spriteDirection == -1)
        {
            effects = SpriteEffects.FlipVertically;
        }
        for(int k = 1; k <= numUnits; k++)
        {
            Texture2D image = unit;
            if(k == numUnits && tip != null)
            {
                image = tip;
            }
            Vector2 pos = origin + direction * (increment * (k - 1) + unitLength / 2f);
            Color color = Lighting.GetColor((int)(pos.X / 16f), (int)(pos.Y / 16f));
            sb.Draw(image, pos - Main.screenPosition, null, GetAlpha(color), angle, new Vector2(unit.Width / 2, unit.Height / 2), 1f, effects, 0f);
        }
        return false;
    }

    public virtual Color GetAlpha(Color color)
    {
        return color * ((255 - projectile.alpha) / 255f);
    }
}}
 
Last edited:
From what I can see, diagonal rectangular hitboxes can also be done with the in-game AABB line check. You get the starting position from Projectile.Center, and the ending position by Adding/Subtracting with Projectile.velocity. Add the width and you got a diagonal hitbox. The following code is from ExampleLastPrismBeam.

C#:
        public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) {
            // If the target is touching the beam's hitbox (which is a small rectangle vaguely overlapping the host Prism), that's good enough.
            if (projHitbox.Intersects(targetHitbox)) {
                return true;
            }

            // Otherwise, perform an AABB line collision check to check the whole beam.
            float _ = float.NaN;
            Vector2 beamEndPos = Projectile.Center + Projectile.velocity * BeamLength;
            return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), Projectile.Center, beamEndPos, BeamHitboxCollisionWidth * Projectile.scale, ref _);
        }
 
Back
Top Bottom