Skip to content

Combat

The action-combat substrate. A character’s hitboxes and hurtboxes are Jolt sensor bodies that ride the character’s skeleton bones; Jolt’s own collision detection reports the overlaps, and the engine hands them to your game as Combat.onHit / onStay / onExit events. You author the volumes’ shape and placement in Blender (bone-parented meshes in dedicated collections) — no hitbox geometry in code — or build them by hand from Wren. Underneath, the same scene also exposes the swept/overlap/pierce shape queries on Physics.

The engine detects; your game owns health. There is deliberately no HP, damage, or death type in the engine. A hit carries an opaque payload — a plain number you set on the hitbox and read back in the callback (typically an entity id, or the raw damage) — and the engine passes it through untouched. What a hit means (how much damage, armor, elements, procs, i-frames, death) is 100% your game’s Wren code. This keeps the engine health-model-agnostic and your game unconstrained.

The whole surface — Shape, Hitbox, Hurtbox, Hit, Combat, node.hitboxes/hurtboxes, node.attachToBone, and the Physics.* combat queries — is a frozen contract. Detection is governed by ADR 0069 (Jolt sensors), which supersedes ADR 0066’s shape-cast detection; the “engine detects, game owns health” boundary and the opaque payload are unchanged from ADR 0066. See the Blender Combat Demo, which loads Blender-authored volumes and lands strikes through Combat.onHit.

Each hit/hurt box is a kinematic Jolt sensor snapped onto its bone every frame (before the physics step), on a dedicated combat collision layer that only pairs hit ↔ hurt (never world/dynamic bodies). Jolt’s contact queue is the sole driver: the engine partitions the drain — pairs where both bodies are combat volumes become combat events, everything else stays the generic Physics.onContact* callbacks. A hitbox never strikes a hurtbox that shares its owner.

Anti-tunnelling caveat. Jolt sensors have no continuous collision detection (CCD), so overlap is sampled frame-by-frame. A realistic bone-driven swing (centimetres per frame) is caught reliably; a hitbox teleported many units past a thin target in a single frame can miss. Swept anti-tunnel for sensors is tracked as PLM-221. (The Physics.castShape query below is swept and remains the tool for one-shot hitscan/lunge checks that must not tunnel.)

An immutable capsule/box/sphere, reused by the box factories and the Physics.* queries. (Convex-hull volumes are built internally from Blender mesh vertices; a hand-authored Shape.convexHull factory is deferred — see ADR 0069.)

Returns: Shape — A Y-up capsule (halfHeight is half the cylinder segment; total height ≈ 2*halfHeight + 2*radius).

Returns: Shape — A box with the given half-extents.

Returns: Shape — A sphere.

The intended workflow: author hit/hurt volumes as meshes in Blender, so shape and placement iterate where the art does. The convention blend_load reads (ADR 0069):

  • A hit/hurt volume is a mesh given a Blender Passive rigid body with “Animated” enabled (kinematic), bone-parented to the bone it rides.
  • Placed in a dedicated collectionPlume_Hitboxes or Plume_Hurtboxes. The collection decides the role; the object name is the tag (e.g. an object named swordhitboxes["sword"]).
  • Shape comes from the rigid body (box / sphere / capsule / convex hull); the mesh AABB gives the half-extents, or the mesh verts the hull.

The Plume3D Blender add-on ships a Combat Volumes panel: select mesh(es), click Mark Hitbox / Mark Hurtbox, and it sets the Passive+Animated rigid body and moves the object into the right collection. On instantiate, each volume becomes a bone-attached sensor Hitbox/Hurtbox (its Jolt rigid body is skipped — it’s a sensor, not a physics collider) and surfaces on node.hitboxes / node.hurtboxes.

A capsule/box/sphere/hull that strikes hurtboxes while active — a bone-attached Jolt sensor. Author it in Blender (above) and reach it via node.hitboxes, or build one from Wren by attaching it to a node (its scene-world anchor and the scene that detects it); optionally pass an AnimController and a bone name so the box rides that bone in scene-world (pass null / "" to ride the node directly). A hitbox never strikes a hurtbox that shares its owner, and by default strikes each victim once per swing (see the dedup rules under independent / reHitSeconds).

Hitbox.capsule(node, controller, bone, radius, halfHeight)

Section titled “Hitbox.capsule(node, controller, bone, radius, halfHeight)”

Returns: Hitbox — A capsule hitbox bound to node (and, if given, controller’s bone).

Hitbox.box(node, controller, bone, hx, hy, hz)

Section titled “Hitbox.box(node, controller, bone, hx, hy, hz)”

Returns: Hitbox — A box hitbox (half-extents).

Hitbox.sphere(node, controller, bone, radius)

Section titled “Hitbox.sphere(node, controller, bone, radius)”

Returns: Hitbox — A sphere hitbox.

  • layer / layer=(mask) (Num) — the bitmask of hurt-layers this hitbox strikes. 0/0xFFFFFFFF = any.
  • tag / tag=(name) (String) — a label carried into the Hit (e.g. "sword", "kick"). Blender-authored volumes tag from the object name.
  • owner / owner=(id) (Num) — an id shared across a combatant’s boxes; a hitbox never strikes its own owner.
  • payload / payload=(n) (Num) — opaque; carried into the Hit untouched (your damage / entity id).
  • enabled / enabled=(flag) (Bool) — off = the sensor is deactivated and generates no contacts (a cheap game-driven active window).
  • independent / independent=(flag) (Bool) — opt this hitbox out of the shared per-owner swing-dedup so it lands its own hit independent of the combatant’s other hitboxes (a multi-part boss where each limb should register). Default false.
  • reHitSeconds / reHitSeconds=(n) (Num) — re-strike a persisting overlap every n seconds (an aura / damage-over-time). 0 (default) = once per swing until the overlap ends and re-begins.

Bind an AnimController so the box rides its bone via the controller’s pose. Blender-authored volumes are created before the character’s controller exists, so call this after you build the controller — it links the box to the live skeleton. (Hand-built boxes pass the controller to the factory instead.)

Gate the hitbox’s active-state to a named animation hit-window on controller — it strikes only while that window is open. Windows are authored as Blender pose-markers (a hitbox:swinghitbox:swing:end marker pair). Unbound, a hitbox is active whenever enabled.

A capsule/box/sphere/hull that can be struck — also a bone-attached Jolt sensor. Same factory shape as Hitbox (attach to a node, and optionally a controller + bone), authored in Blender or built from Wren. Disabling it is i-frames (invulnerability) or death.

Hurtbox.capsule(node, controller, bone, radius, halfHeight)

Section titled “Hurtbox.capsule(node, controller, bone, radius, halfHeight)”

Returns: Hurtbox

Hurtbox.box(node, controller, bone, hx, hy, hz)

Section titled “Hurtbox.box(node, controller, bone, hx, hy, hz)”

Returns: Hurtbox

Hurtbox.sphere(node, controller, bone, radius)

Section titled “Hurtbox.sphere(node, controller, bone, radius)”

Returns: Hurtbox

  • layer / layer=(mask) (Num) — the layer(s) this hurtbox occupies (matched against a hitbox’s mask).
  • tag / tag=(name) (String), owner / owner=(id) (Num), payload / payload=(n) (Num) — as on Hitbox.
  • enabled / enabled=(flag) (Bool) — false is i-frames: the sensor is off, so it can’t be struck.

As hitbox.bindController — link a Blender-authored hurtbox to the character’s AnimController so it rides its bone.

The read-only event handed to a Combat.onHit / onStay / onExit closure.

  • attacker (Num) — the striking hitbox’s owner.
  • victim (Num) — the struck hurtbox’s owner.
  • hitboxTag (String) — the striking hitbox’s tag.
  • point (List) — [x, y, z] world contact point.
  • normal (List) — [x, y, z] world contact normal.
  • payload (Num) — opaque, carried from the hitbox untouched. Your HP/entity boundary.

Three lifecycle events over a hit ↔ hurt overlap. Register a closure per scene per event; the engine calls it at the tail of update() — you never call a resolve/tick yourself (a game-called tick would re-enter the Wren VM, which is forbidden; the engine ticks combat for you, the same way it ticks behaviour trees and cameras). Inside the closure you read the hit and mutate your own game state. Health is yours.

Register fn(hit) for a fresh strike — once per swing per attacker by default (a shared per-owner ledger, reset when the attacker has no active hitbox), honouring hitbox.independent and reHitSeconds. Fires on the first frame a hit/hurt pair is actively overlapping, whether that is a new contact or a box activating onto an already-touching victim. Pass null to clear.

Register fn(hit) for each frame a hit ↔ hurt pair keeps overlapping — auras, pressure, sustained contact. Pass null to clear.

Register fn(hit) for when a hit ↔ hurt overlap ends — release per-target state (e.g. a burn tick). Pass null to clear. onExit fires on natural separation; if you destroy a box (drop your last reference to it so it’s garbage-collected) while it’s still overlapping, no onExit is synthesized for that box — so release any per-overlap state you opened when you remove a box. Disabling a box (enabled = false, the i-frames / death pattern) keeps it and still fires onExit when contact ends.

import "engine" for Scene, Resource, Combat
var scene = Scene.new()
var res = Resource.loadBlend("CombatVolumes.blend")
var attacker = res.instantiate("Fighter", scene) // Blender-authored hit/hurt volumes
var target = res.instantiate("Fighter", scene)
// The GAME assigns identity: owner groups a combatant's boxes (a hitbox never strikes its own owner).
for (hb in attacker.hitboxes.values) { hb.owner = 1; hb.payload = 25 } // opaque: THIS game reads it as damage
for (hu in attacker.hurtboxes.values) { hu.owner = 1 }
for (hb in target.hitboxes.values) { hb.owner = 2 }
for (hu in target.hurtboxes.values) { hu.owner = 2 }
var hp = 100 // health is a plain game field — no engine HP type
Combat.onHit(scene) { |hit|
hp = hp - hit.payload // the engine detected the strike; the game decides what it costs
if (hp <= 0) {
for (hu in target.hurtboxes.values) hu.enabled = false // death: the corpse stops taking hits
}
}

Added to Node by the combat substrate.

Returns: Map(tag → Hitbox) / Map(tag → Hurtbox) — the Blender-authored combat volumes on an instantiated character, keyed by tag (the Blender object name). Empty when the .blend had none. Set owner / payload and call bindController on the boxes to wire a character up after instantiate.

node.attachToBone(character, controller, bone)

Section titled “node.attachToBone(character, controller, bone)”

Make this node follow character’s bone every frame (LateUpdate) — a stateful weapon/VFX socket, engine-driven and Blender-authorable (bone-parent in Blender, or call this at runtime). Pass bone "" (or a null controller) to track the character node’s world transform.

Stop following (releases the socket).

Swept / at-pose / piercing narrow-phase queries over the scene’s rigid bodies (ADR 0066, unchanged). Each returns a List of RaycastHit (near→far), mapping each hit body back to its Node. mask 0/0xFFFFFFFF = any gameplay layer. These are queries you call directly (not the sensor event stream) — use castShape for a swept, tunnel-proof one-shot check.

Physics.castShape(scene, shape, fx, fy, fz, tx, ty, tz, mask)

Section titled “Physics.castShape(scene, shape, fx, fy, fz, tx, ty, tz, mask)”

Returns: List of RaycastHit — every body a shape sweep from (fx,fy,fz) to (tx,ty,tz) crosses. The anti-tunnel primitive for fast one-shot attacks.

Physics.overlapShape(scene, shape, x, y, z, qx, qy, qz, qw, mask)

Section titled “Physics.overlapShape(scene, shape, x, y, z, qx, qy, qz, qw, mask)”

Returns: List of RaycastHit — every body a shape overlaps at a pose (position + rotation quaternion x,y,z,w). True narrow-phase geometry (catches a large body whose center is outside the shape, unlike overlapSphere).

Physics.raycastAll(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)

Section titled “Physics.raycastAll(scene, ox, oy, oz, dx, dy, dz, maxDist, mask)”

Returns: List of RaycastHit — every body a ray from (ox,oy,oz) along (dx,dy,dz) up to maxDist pierces. The piercing-hitscan primitive.