Every action game ends up wanting a sweetspot: the head of the hammer should hurt more than the handle, and the tip of the spear should reward the player that keeps exactly the right distance. Smash players have been chasing Marth’s tipper for twenty-five years.
The first time you build one, the tempting route is to script it: on hit, you measure how far along the weapon the impact landed, and if it is past 80% of the blade you apply a bonus. This works fine for one sword. But the moment you add a flail that measurement means nothing, someone asks why the bonus fires when you clip a wall, and you end up maintaining a special case per weapon.
When I built the hitbox system of my Quod Combat Framework I wanted sweetspots without writing any per-weapon code, and in the end the answer was really simple: a sweetspot is one extra capsule and one integer. This post is about that trick, and also about how I author melee hitboxes in general: which shape to use, which parameters a hitbox row needs, how the animation timeline drives it all and where the data should live.
Everything is a capsule
First a little bit of context: the framework does not use overlap events for melee. When the damage window of an attack opens ( more on that window later ), a throwaway damage dealer object starts sweeping the attack’s capsules every frame, from last frame’s pose to this frame’s pose. Whatever the sweeps touch is a candidate hit, and when the window closes the capsules are gone.
Every one of those hitboxes is a capsule, we do not use boxes or per-triangle mesh collision.
A capsule is a segment with a radius, and if you think about it, a sword, a spear, a club, an arm or a tail are more or less that same shape. This is why capsules work so well for melee:
- They can be authored socket to socket. Define a capsule between two bones and it follows the animation for free, nobody has to keyframe hitbox positions frame by frame.
- Capsule sweeps are cheap, and the engine already ships them ( SweepMultiByChannel with a capsule shape ).
- They are honest to the player. Mesh-accurate collision sounds better than it really is: a hit from a clipping cloth flap feels unfair, and nobody can see the difference during a 300 ms swing anyway. A slightly generous capsule that always behaves the same is much better than an accurate mess.
From Software has been shipping with this shape for over a decade. If you have watched Zullie the Witch’s breakdowns of Dark Souls and Elden Ring you have seen it: weapons carry a few capsules laid along the blade, and the swing damages whatever those capsules pass through.
In practice you only need two capsule shapes:
UENUM()
enum class EQuodHitboxShape : uint8
{
SingleBone, // one bone, capsule extends along a chosen axis
BoneToBone // capsule stretched between two bones, every frame
};
USTRUCT()
struct FQuodHitboxDef
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
EQuodHitboxShape Shape = EQuodHitboxShape::SingleBone;
UPROPERTY(EditAnywhere)
FName StartBone;
UPROPERTY(EditAnywhere)
FName EndBone; // BoneToBone only
UPROPERTY(EditAnywhere)
float Radius = 0.f;
UPROPERTY(EditAnywhere)
float Length = 0.f; // SingleBone: reach along the axis
UPROPERTY(EditAnywhere)
float ExtraLength = 0.f; // BoneToBone: padding past the end bone
UPROPERTY(EditAnywhere)
FVector LocalOffset = FVector::ZeroVector;
UPROPERTY(EditAnywhere)
TEnumAsByte<EAxis::Type> Axis = EAxis::Z;
UPROPERTY(EditAnywhere)
bool bCheckObstacles = true;
};
( I trimmed the struct to the important fields, the real one has some editor helpers on top )
SingleBone covers most weapons: you anchor it on the weapon bone and extend it along an axis. BoneToBone is for things that bend, like flails, chains and tails. The capsule is recomputed between the two bones every frame, so it stays glued to the simulation no matter what the physics does, and ExtraLength adds some padding past the end bone so the tip of the chain still counts.
My recommendation is to author everything as SingleBone and only go BoneToBone when the weapon really bends.
Rank and group: the whole trick
Every active hitbox in the framework also has two small integers on it, a Group and a Rank.
Group tells the system which hits count as the same hit. Hitboxes that share a group are treated as a single hitbox when deduplicating hits: once the group has hit a target, that group can not hit that target again during the swing.
Rank decides which hitbox applies when several of them land at the same time. When more than one box of the same group connects on the same frame, the lowest rank wins. The resolution is really simple, a dozen lines in the sweep loop:
// Several capsules may have swept through the same target this frame.
// Keep one hit per target: the one with the lowest rank.
TMap<AActor*, FQuodHitboxHit> SelectedHits;
for (const FQuodHitboxHit& Hit : HitsThisFrame)
{
if (FQuodHitboxHit* Selected = SelectedHits.Find(Hit.Target))
{
if (Hit.Rank < Selected->Rank)
{
*Selected = Hit;
}
}
else
{
SelectedHits.Add(Hit.Target, Hit);
}
}
// Apply damage once per target, with the winning hitbox's payload.
Now let’s see the sweetspot recipe with a spear:
- Normal hitboxes: a bone-to-bone capsule down the shaft, stopping below the tip. Group 0, rank 0.
- Sweetspot: a second capsule on the tip. Group 0, rank 1, pointing at a damage row with the bonus.
When a careless poke clips the target with both capsules, the rank 0 shaft wins and normal damage applies. When the player spaces the attack so that only the tip touches, the sweetspot is the only candidate in its group and the bonus lands. As you can see, nobody measured any distance and nobody wrote spear code, the priority rule resolves it by itself, and because both boxes share a group the target is never hit twice by the same thrust.
You can also flip it: author the bonus box at rank 0 and it will dominate instead, whenever the hammer head touches, the head damage applies, even if the haft also brushed the target. So choosing between a yielding sweetspot and a dominant one is a matter of changing one integer, and different weapons in the same game can choose differently.
One tip: color your debug draw using these two numbers from day one. Mine picks the hue from the rank and darkens it by the group, so with one look at the capsules you know which box wins where, which is incredibly useful when tuning attacks.
Smash does the same thing
Smash Ultimate is the best public reference for this pattern. Every attack is a handful of spheres plus what they call “extended hitboxes”, a sphere stretched between two points, which is basically a capsule with another name. Each hitbox has an ID, and when several hitboxes of the same attack connect on the same frame, the lowest ID takes priority. SmashWiki’s hitbox page documents all of this, even with per-move hitbox visualizations.
That is how Marth’s tipper exists: the sword carries hitboxes along the blade plus a dedicated one at the tip, with its own damage and knockback. It is the same idea, a few capsules plus a priority number. If one of the most tuned fighting games out there ships sweetspots this way, you can be pretty confident shipping yours the same way.
Damage windows on the montage timeline
None of these capsules is activated from code either. Hitboxes fire from an anim notify state placed on the attack montage: the damage window. NotifyBegin activates the capsules of the window and NotifyEnd removes them. The window is the active frames of the attack, the same concept from fighting games, but here they live on the montage as a colored bar that a designer can see and drag in Persona without asking a programmer.
UCLASS()
class UQuodANS_DamageWindow : public UAnimNotifyState
{
GENERATED_BODY()
public:
// Empty = derive the row name from the animation name.
UPROPERTY(EditAnywhere)
FName HitboxRow;
// Empty = same row name as HitboxRow.
UPROPERTY(EditAnywhere)
FName DamageRow;
UPROPERTY(EditAnywhere)
uint8 Rank = 0;
UPROPERTY(EditAnywhere)
uint8 Group = 0;
// NotifyBegin: resolve the rows against the equipped weapon,
// activate the capsules with this Rank/Group.
// NotifyEnd: remove exactly this window's capsules.
};
The notify is kept really simple on purpose. It only stores names: which hitbox row and damage row to use, and the rank and group of everything it activates.
This is how the sweetspot recipe looks when authoring. On the timeline, the spear thrust has two overlapping damage windows: the normal window at rank 0, group 0, covering the whole swing, and the sweetspot window at rank 1, same group, pointing at the tip capsule. And since they are just bars on a timeline, you can make the sweetspot window shorter, so the tip bonus only exists at full extension, and authoring that is simply dragging an edge. When any of the two windows ends, it removes only its own capsules and nothing else.
Two conventions keep this from becoming a lot of manual work:
- Empty fields resolve by naming convention. If the notify doesn’t name a row, the animation name is the row name: the montage
Attack_Spear_Light1looks up the rowAttack_Spear_Light1. Most windows end up shipping with zero properties touched. - Override the display name of the notify. Mine label themselves with their rank and group ( “DAMAGE r0 g0” ), so a montage with a sweetspot shows two readable bars and nobody has to click into the properties to understand the attack.
Once the hit frames live on the timeline, you will want the same for every other combat mechanic with timing. Same thing can be done for parry windows, i-frames on the dodge montage, input-buffer windows and cancel windows, all of them can copy the pattern of the damage window. We may go over some of those in future articles.
What else belongs in the attack row
The capsules are the most visible part, but a production attack row carries a few more decisions. If you are building your own system, these are the parameters I would not skip.
Three separate hitbox arrays. My attack rows carry damage hitboxes, physical hitboxes and environment hitboxes, instead of one single array for everything. The physical set is what makes your weapon rebound off a wall mid-swing, and it bounces only off near-vertical geometry ( it ignores the floor and characters ), because nothing feels worse than a greatsword bouncing off a small rock. The environment set exists only to spawn sparks and impact effects on the world, once per swing, and only if you did not hit anyone. All three can have different shapes, your damage reach does not have to match your rebound reach or your FX reach.
A multi-hit policy per attack. Mine is an enum:
enum class EQuodHitPolicy : uint8
{
FirstHitOnly, // the first contact ends the attack
OncePerTarget, // each target can be hit once per swing
Unlimited, // no dedup at all
RehitAfterDelay // the same target can be re-hit after N seconds
};
OncePerTarget is your normal swing. FirstHitOnly is for attacks where you only care about the first contact, like a grab attempt. RehitAfterDelay with a half second window is my default, and it is what you want for long windows like a spinning attack: the same enemy can be hit again, but on a timer instead of every frame.
A wall check, with an opt-out. For every candidate hit, we do a line trace from the attacker toward the impact point on an obstacle channel, and if a wall is in between, the hit is rejected. The per-hitbox bCheckObstacles flag is there because sooner or later some attack needs to ignore the global rule ( ex: a shockwave that is supposed to travel through walls, or a boss arm wrapping around a pillar ). Every rule like this one ends up needing exceptions, so it is better to have the opt-out in the data from the start.
The weapon owns the data
The last authoring decision is where the rows live, and I think this one matters more than any of the above: the hitbox and damage tables belong to the weapon, not the animation.
Each weapon’s data asset carries its movesets per stance ( one-handed, two-handed, dual wield ), its own hitbox table and its own damage table. Remember that the damage window on the montage stores only row names. When the window opens, those names are resolved against the tables of the equipped weapon.
So one shared greatsword montage serves every greatsword in the game, and each weapon brings its own reach, sweetspot and numbers. When a designer decides that the spear tip bonus should be lower, they edit one row in one weapon’s table, and nothing else in the game changes.
Recap
- A sweetspot is one extra capsule and one integer. Same Group, different Rank: when several boxes of one group connect in the same frame the lowest rank wins, so a higher-rank sweetspot only applies when it is the only box touching. Flip the ranks and it becomes dominant instead. Smash Ultimate resolves its hitbox IDs the same way.
- Author capsules socket to socket: one bone plus an axis and a length for rigid weapons, bone-to-bone for flails and tails. They follow the animation for free, and the shape is honest at the speed of a real swing. Dark Souls has been shipping with it for a decade.
- Hitboxes fire from damage window notify states on the montage: the window carries the rank and group, the active frames are bars that a designer can drag, and a sweetspot is two overlapping windows. The rows resolve by name against the equipped weapon, so shared animations serve every weapon and tuning is always a data edit.


