Hit Boxes
Gameplay hit regions authored in Flash - so the designer places them, sees them, and
animates them right alongside the art - but realised as engine-native colliders the
programmer can work with. Not Flash's hitTest: real Area3Ds, Area2Ds and trigger
colliders, in your physics world, on your layers.
Quick start#
1. Draw the region in Flash. Convert it to a MovieClip and export it for ActionScript under one of three reserved linkage names. No ActionScript is needed - only the name is read.
| Export name | Becomes |
|---|---|
HitBox |
A box hit region |
HitBoxCircle |
A circle / sphere hit region |
HitBoxCapsule |
A capsule hit region |
2. Name the placement. Give it an instance name in the Properties panel - Head,
Body, Sword. That name is how you identify it in code.
3. Listen for overlaps. Colliders are generated by default, so there is nothing to switch on.
// Unity
void OnEnable() { player.OnSwfCollisionEntered += OnHit; }
void OnDisable() { player.OnSwfCollisionEntered -= OnHit; }
void OnHit(string name, string path, Collider other)
{
if (name == "Sword")
{
DealDamage(other);
}
}# Godot
func _ready() -> void:
swf.collision_entered.connect(_on_collision_entered)
func _on_collision_entered(name: String, path: String, other: Node) -> void:
if name == "Sword":
deal_damage(other)That is the whole loop: author, name, react. Everything below is detail.
Authoring one#
The instance name is the region's identity - it arrives with every event and it is what
you match on in code. Nested placements get a dot-path (Body.Head), so two regions can
share a leaf name at different depths.
The symbol stays fully visible while you author in Flash, and is never rendered at runtime in either engine - whatever its artwork, and whatever alpha or visibility the animator left on it. Colour them however is easiest to see; nobody but you will.
Because the three names are matched exactly, they are also unavailable as ordinary playable clips - see MovieClip Library.
The active set is frame-scoped#
A hit region lives on the timeline, so it exists on exactly the frames the animator drew it on. A hurtbox placed on frames 5-10 of an attack exists for those six frames and no others, at the position, rotation and scale it has on each one. Nothing to enable, nothing to schedule - the animation is the schedule.
That holds for every timeline, not just the root one: regions inside a nested clip come and go with that clip's own playhead.
Seeing them while you work#
Select the player.
- Unity draws the active regions as yellow wire shapes, which works even with collider generation switched off, and the pooled colliders draw Unity's standard green gizmo on top. They also show up in the Physics Debugger (Window > Analysis > Physics Debug) and in the Hierarchy while they are active.
- Godot draws the bound areas with the rest of your collision under Debug > Visible Collision Shapes.
Because the regions are frame-scoped, scrubbing the frame slider in the inspector is the fastest way to check that a hurtbox covers the frames you meant it to.
Reacting to them#
There are two different things to listen for, plus a query:
- Overlap - something touched one of your regions. This is the collision callback.
- Lifecycle - a region appeared on or left the composed frame as the timeline plays. Not a collision. It fires whether or not colliders are generated, so you can turn generation off entirely and drive your own logic from the reported transforms.
- The active set - ask what is live on this frame instead of listening.
Unity#
// Overlaps - something touched one of your regions
void OnEnable()
{
player.OnSwfCollisionEntered += HandleSwfCollisionEntered;
player.OnSwfCollisionExited += HandleSwfCollisionExited;
}
void OnDisable()
{
player.OnSwfCollisionEntered -= HandleSwfCollisionEntered;
player.OnSwfCollisionExited -= HandleSwfCollisionExited;
}
void HandleSwfCollisionEntered(string name, string path, Collider other)
{
if (name == "Body")
{
TakeDamage(other);
}
}
void HandleSwfCollisionExited(string name, string path, Collider other)
{
// `other` is null here if it was destroyed while still overlapping.
StopBleeding();
}// Lifecycle - a region appeared on or left the frame
void OnEnable()
{
player.OnHitboxSpawned += (name, path) => Debug.Log("appeared: " + name);
player.OnHitboxDespawned += (name, path) => Debug.Log("left: " + name);
}// The active set, right now
foreach (SwfMesh.SwfHitbox hb in player.GetActiveHitboxes())
{
Debug.Log(hb.path + " at " + hb.center + " size " + hb.size);
}// Identifying a region from the OTHER side of an overlap
void OnTriggerEnter(Collider other)
{
SwfMesh.SwfHitboxInfo info = other.GetComponent<SwfMesh.SwfHitboxInfo>();
if (info == null)
{
return;
}
Debug.Log("hit by " + info.owner.name + " region " + info.hitboxName);
}Both event pairs are also wired to the inspector as UnityEvents (onSwfCollisionEntered,
onHitboxSpawned, and so on), so a designer can hook one up without writing code. If you
would rather query than listen, GetHitboxCollider(nameOrPath) and GetHitboxColliders()
hand you the live colliders.
Unity raises overlaps during the physics step, where Unity callbacks belong, and it is
safe to change frame, stop or reload the player from inside the handler. Unity also
guarantees the exit in three cases its own physics would quietly drop: a region that leaves
the frame while something is still inside it, a player being reloaded or destroyed, and an
object destroyed while overlapping (that last one arrives with other as null).
You do not need to add a Rigidbody to anything. Unity sends no trigger message unless
one side of the pair has a body, and as of 0.2.0 the plugin puts a kinematic one on every
generated collider - so a region overlapping ordinary static level geometry reports
normally, with nothing for you to set up. (Before 0.2.0 it did not, and this page told you
to add one yourself.) Direct queries such as Physics.OverlapBox work against the pooled
colliders like any other.
Godot#
# Overlaps - something touched one of your regions
func _ready() -> void:
swf.collision_entered.connect(_on_collision_entered)
swf.collision_exited.connect(_on_collision_exited)
func _on_collision_entered(name: String, path: String, other: Node) -> void:
if name == "Body":
take_damage(other)
func _on_collision_exited(name: String, path: String, other: Node) -> void:
# `other` is null here if it was freed while still overlapping.
stop_bleeding()# Lifecycle - a region appeared on or left the frame
func _ready() -> void:
swf.hitbox_spawned.connect(func(name, path): print("appeared: ", name))
swf.hitbox_despawned.connect(func(name, path): print("left: ", name))# The active set, right now
func log_active_regions() -> void:
for hb in swf.get_active_hitboxes():
print(hb.path, " at ", hb.transform.origin, " size ", hb.size)get_hitbox_area("Sword") hands you the Area2D or Area3D realising that region right
now, and get_hitbox_areas() hands you all of them - useful for reading a live position or
wiring up something engine-specific.
Godot fires overlaps one idle frame after the physics step, and that delay is
deliberate: it is what makes it safe to change frame, stop, or reload the player from
inside the handler, which is most of what you want a hit callback for. One consequence to
know about - if whatever you touched was freed in that same instant, the exit still fires,
with other as null. The name and path are the useful half, and dropping the event would
leave your own state believing it was still overlapping.
What the events carry#
name and path always identify your region; other is the Area or physics body that
touched it. Areas and all three body types report, and a player's own regions never report
against each other.
The active-set entries carry the name, the dot-path, the kind, the player-local transform
and the realised world-unit dimensions - the same numbers the pooled collider was built
from, so what you read and what you collide with can never disagree. Godot 2D reports a
Transform2D and a Vector2 size, and omits the height a flat plane has no room for.
The lifecycle pair is hitbox_spawned / hitbox_despawned in Godot and OnHitboxSpawned /
OnHitboxDespawned in Unity. (They were called hitbox_entered / hitbox_exited and
OnHitboxEntered / OnHitboxExited before 0.2.0; the old names read as collision, which is
not what they do.)
Setting regions up individually#
A character's regions are rarely all the same: the body and the sword want different layers, and a region used by one attack wants to be off the rest of the time.
Player-level defaults apply to every generated collider - set them once and the whole
character lands on the layers you want. In Godot those are collision_layer and
collision_mask; in Unity, colliderLayer and colliderReportMask.
A per-region list then overrides them: collider_configs in Godot, hitboxConfigs in
Unity. Each entry carries:
| What it does | Godot | Unity |
|---|---|---|
| Which region this entry is for - instance name, or full dot-path | hitbox |
hitbox |
| Off generates no collider at all | enabled |
enabled |
Its layers - a bitmask in Godot, a single layer in Unity (-1 inherits) |
collision_layer |
layer |
| What it reports against | collision_mask |
reportMask |
| Whether it reports overlaps at all | monitoring |
reportOverlaps |
| Whether others can detect it | monitorable |
- |
| Area priority | priority |
- |
| Its extruded depth, 3D only | depth |
depth |
Three of those are Godot-only, and Unity's mask filters events rather than physics - see Limitations.
Keying. An instance name covers every region with that leaf name at once - one entry
named Sword configures the Sword in all three of your attack clips. A full dot-path
singles one out. Where both match, the name entry wins.
Why a list on the player, and not settings on the collider. Reaching into a generated
carrier and setting its layer yourself will not stick: the carriers are pooled, so the mask
you set for Sword lands on Head the moment that slot recycles, and a reload rebuilds
them all with stock settings. List entries are keyed to the region's own identity and
re-applied every time a region is bound, so neither case can lose them. Edit an entry
and it takes effect at once - no reload, no restart.
Filling the list. The inspector shows a read-only list of every hit region the
selected clip can place, read straight from the .swf - so it is there before you have
pressed play, and includes regions that only appear on frame 200. Populate Collider List
(Godot) or Populate From SWF (Unity) turns that into editable entries, one per region.
The list starts empty on purpose, so a project with hundreds of players does not carry settings nobody set. Pressing the button again fills in anything new and leaves your edits alone.
Flat or extruded - 3D only#
A Flash region is flat, and 3D wants a volume. Flat is the default - a thin shape sitting on the player's plane, which behaves like 2D collision inside a 3D scene. Extruded gives the shape a depth along local Z, centred on the plane.
| Unity | Godot 3D | |
|---|---|---|
| Flat or extruded | colliderMode |
collider_mode |
| Depth when extruded | colliderDepth |
collider_depth |
| Per-region depth | the depth on a list entry (above) |
the depth on a list entry (above) |
Capsules ignore both, being three-dimensional already. Godot 2D has no third dimension, so none of these exist on it.
Both engines also keep an older depth-only dictionary - colliderDepthOverrides /
collider_depth_overrides, keyed by instance name first and dot-path second. It still
works, but the list entry is the better home, and the entry wins where both name the same
region.
What each kind becomes#
| Unity | Godot 3D | Godot 2D | |
|---|---|---|---|
| Generated by | generateCollisionShapes (on) |
generate_collision_shapes (on) |
generate_collision_shapes (on) |
| Box | BoxCollider |
BoxShape3D |
RectangleShape2D |
| Circle | SphereCollider (flat) · Z-axis CapsuleCollider (extruded) |
CylinderShape3D - a true thin disc |
CircleShape2D |
| Capsule | in-plane CapsuleCollider |
CapsuleShape3D |
CapsuleShape2D |
| Carrier object | trigger collider under a container | Area3D + CollisionShape3D |
Area2D + CollisionShape2D |
| Identifying it from the other side | SwfHitboxInfo component |
Area metadata | Area metadata |
| Overlap events on the player | OnSwfCollisionEntered / OnSwfCollisionExited |
collision_entered / collision_exited |
collision_entered / collision_exited |
Each kind is solved from the placement transform and the region's local bounds, with identical maths in both engines:
- Box - the oriented bounding box. The transformed size is used directly and the rotation is preserved; skew folds into the size.
- Circle - the radius is the average of the two in-plane radii,
(w + h) / 4. A scale-stretched circle is an ellipse, and no physics primitive matches an ellipse, so it is squared off to the nearest circle. Author stretched regions asHitBoxCapsule. - Capsule - Flash cannot draw a true capsule, so the artwork is only a stand-in. The shorter transformed extent becomes the end-cap diameter, the caps sit at the ends of the longer extent, and the capsule runs along that long in-plane axis. Its depth is its diameter, which is why the depth controls above do not apply to it.
Scale and skew are baked into the shape's dimensions rather than into a transform, because neither engine's physics supports scaled collision transforms. The collider keeps a proper rotation.
Two things hold in all three columns:
- The carriers are pooled, not per symbol. A
.swfwith fifty hit regions but two active on this frame keeps two colliders, bound and released as regions enter and leave. Shape resources are swapped in place as the pool is rebound. - The carriers never reach your saved scene. They are internal in Godot and marked not-to-save in Unity, so they do not clutter the file you commit. Your settings do persist - they live on the player, which is what lets them survive the pool being rebound - and each carrier is named for its instance name and carries the path, name and kind, so the other side of an overlap can ask what hit it.
Godot 2D takes the placement as authored, with no conversion: a Flash hit region is drawn in a plane, and Godot 2D is that plane.
Limitations#
- A stretched circle is an ellipse, and no physics primitive is an ellipse. It realises
as a circle of the averaged radius. Author stretched regions as
HitBoxCapsule, or asHitBoxif a rectangle will do. - Unity has no thin-disc primitive. A flat circle becomes a sphere, which bulges by its radius in Z; an extruded circle becomes a Z-axis capsule with rounded caps, whose height is floored at twice the radius. Godot 3D uses true cylinder discs and Godot 2D a plain circle, so this is the one place the two engines genuinely differ in shape.
- An attached child contributes no hit regions while it is riding inside a host clip. Its own transform is unrelated to where the host draws it, so its colliders would land in the wrong place. Regions on the host itself are unaffected.
- Two placements sharing an instance name under the same parent on the same frame collide on identity, and the first one wins. Give them distinct names.
- Matching is exact.
HitBox,HitBoxCircle,HitBoxCapsule- no package-qualified variants, no prefix matching. - A parked player will not report static scenery until it moves. Physics pairs an area
with a
StaticBodyonly when one of the two moves, so a region sitting still inside a static body reports nothing until the frame changes. Moving bodies -CharacterBody,RigidBody- report immediately. It never shows while a clip is playing; it only bites a player stopped on a single frame over static geometry. - Three per-region settings are Godot-only, and deliberately absent on Unity rather than
present-and-ignored.
monitorableandpriorityhave no Unity equivalent at all. And where Godot gives a region its own physicscollision_mask, Unity cannot - per-collider filtering is not something Unity has, so the Unity field is calledreportMaskand filters which overlaps raise your events, leaving the physics query itself to Project Settings ▸ Physics ▸ Layer Collision Matrix. Similarly Godot'smonitoringis Unity'sreportOverlaps: the collider stays in the scene and only the events are suppressed. A Unity object also sits on exactly one layer, solayertakes a single layer where Godot takes a bitmask.
See also#
- Plugin Features - everything else the plugin adds on top of Flash
- MovieClip Library - how exported symbols become selectable clips
- Scripting API - the playback surface, in C# and GDScript