SwfMesh Plugin documentation

Hit Boxes

⚠️ Closed Beta — SwfMesh is not yet on sale. Contact us for beta access.

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.

csharp
// Unity
void OnEnable()  { player.OnSwfCollisionEntered += OnHit; }
void OnDisable() { player.OnSwfCollisionEntered -= OnHit; }

void OnHit(string name, string path, Collider other)
{
    if (name == "Sword")
    {
        DealDamage(other);
    }
}
gdscript
# 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#

csharp
// 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();
}
csharp
// 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);
}
csharp
// The active set, right now
foreach (SwfMesh.SwfHitbox hb in player.GetActiveHitboxes())
{
    Debug.Log(hb.path + " at " + hb.center + " size " + hb.size);
}
csharp
// 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#

gdscript
# 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()
gdscript
# 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))
gdscript
# 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 as HitBoxCapsule.
  • 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 .swf with 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 as HitBox if 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 StaticBody only 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. monitorable and priority have no Unity equivalent at all. And where Godot gives a region its own physics collision_mask, Unity cannot - per-collider filtering is not something Unity has, so the Unity field is called reportMask and filters which overlaps raise your events, leaving the physics query itself to Project Settings ▸ Physics ▸ Layer Collision Matrix. Similarly Godot's monitoring is Unity's reportOverlaps: the collider stays in the scene and only the events are suppressed. A Unity object also sits on exactly one layer, so layer takes a single layer where Godot takes a bitmask.

See also#