Combat
A melee swing, a thrown crate and a scripted hazard volume are three different damage sources — but they all describe a hit the same way, and one component decides whether it lands. Add a target, add a source, and the arbiter does the rest.
One claim, one judge
Every hit in the kit — melee, thrown, hazard — is filed as a DamageClaim: a target, an amount, who dealt it, which sides it may hit, and optional knockback or stat-effect payloads. Nothing applies damage directly. A claim goes to CombatArbiter.FileClaim, and the arbiter — one per level scene, installed next to the round controller — is the only thing that ever calls IDamageable.ApplyDamage. That is what keeps friendly fire, self-hits and duplicate hits consistent regardless of which system detected the hit.
| Field | Type | What it does |
|---|---|---|
Target | IDamageable | The resolved sink — a DamageableObject or a CharacterDamageable. Required. |
Amount | float | Points of damage. Zero is legal only when the claim also carries a payload (a damage-free shove); negative is always rejected. |
Instigator | GameObject | Who dealt it. For a thrown object this is the thrower, stamped at launch — never the projectile and never whoever owns it now. |
AffectedGroups | CharacterGroupMask | Which CharacterGroups this claim may hit — travels with the claim so validation stays in one place. |
CanHitInstigator | bool | Whether the claim may hit its own instigator (your own thrown chair, your own explosion). |
HitId | int | Dedup token shared by every claim one swing or impact produces. 0 = no dedup window (a one-shot hazard tick). Produced by CombatArbiter.NextHitId(). |
Impulse | Vector3 | Knockback, as a world-space impulse. Zero = none. Obeys the same rules as damage: mask, self-hit, dedup, aliveness. |
TargetEffect | StatEffectDefinition | Effect applied to the target on acceptance (a poison blade, a slow). Null = none. |
InstigatorEffect | StatEffectDefinition | Effect applied to the attacker on acceptance (regen-on-hit). Null = none, and it never travels to the target. |
A claim that fails validation is rejected, not silently dropped — CombatArbiter.OnClaimRejected reports why:
| Field | Type | What it does |
|---|---|---|
InvalidAmount | ClaimRejection | Amount was negative, or zero with no payload to justify it — always a caller bug, and the only rejection that also logs an error. |
MissingTarget | ClaimRejection | The claim carried no target — resolve one with DamageTargets.Resolve first and skip filing when it returns null. |
TargetDead | ClaimRejection | Normal play: the target was already dead when the claim arrived. |
GroupExcluded | ClaimRejection | Normal play: the target's CharacterGroup sits outside the claim's AffectedGroups (friendly fire). |
SelfHitBlocked | ClaimRejection | The claim would hit its own instigator and CanHitInstigator was false. |
Duplicate | ClaimRejection | The same (instigator, hitId, target) triple already landed in the last five seconds. |
// Add the arbiter to a level-root object at runtime
gameObject.AddComponent<CombatArbiter>();
// (The Setup window's Install places one in every generated level)The CombatArbitercomponent is hidden from the Add Component menu on purpose because the Setup window's Install owns it — hand-placing one bypasses the installer's initialization.
What you see when it lands is a separate concern from whether it lands: CharacterFeedback watches the same accepted claim and health change every other system reacts to and plays a particle, a camera shake and a beat of hit-stop for it — see Feedback for the four stock definitions and how to wire your own.
Two kinds of target
Anything hit needs an IDamageable. The kit ships two: DamageableObject for scenery that keeps its own health with no CharacterStats involved (crates, breakable props, the shipped Target Dummy), and CharacterDamageable for characters, which routes every hit onto the existing health stat — so the HUD, the alive/dead event and multiplayer replication all follow with nothing extra to wire.
| Field | Type | Default | What it does |
|---|---|---|---|
maxHealth | float | 45 | Hit points. The object dies when they reach zero. |
group | CharacterGroup | Neutral | Which side this object belongs to for the friendly-fire mask. |
vanishDelaySeconds | float | 10 | Seconds the object stays present after death before it vanishes — time for a death animation. 0 = vanish immediately. |
deathAnimator | Animator | — | Optional. Triggered when the object dies (a collapse clip, e.g. the goblin "Dying"). Empty = no animation, just the delay. |
deathTrigger | string | "Death" | Trigger parameter set on the death Animator when the object dies. |
CharacterDamageable requires CharacterStats on the same object and adds no health field of its own — see Stats & Effects for how the health stat itself is configured. Add DamageReactionAnimator beside either kind of target to fire a Hit Animator trigger on every landed hit (it does nothing if the object has no Animator).
Melee
MeleeAttack reads the Attack action, runs a windup, then opens a child MeleeHitboxfor its active window. The hitbox is a trigger volume — parented under the character so Unity's own rule that colliders sharing a rigidbody never trigger each other keeps the attacker safe from their own swing by construction, not by filtering. Anything the hitbox overlaps while open takes one hit, once per swing.
| Field | Type | Default | What it does |
|---|---|---|---|
hitbox | MeleeHitbox | — | The trigger-volume child this swing opens. Wired by the Setup window on the shipped character. |
damage | float | — | Extra damage on top of the attack stat — usually 0. The stat owns the number; this is a rare override. |
attackStat | StatDefinition | — | The stat whose max is this character's attack power. |
affectedGroups | CharacterGroupMask | Everyone except own | Which sides this attack may hit. |
knockbackForce | float | 0 | Knockback impulse along the attacker's facing, applied to whatever the swing lands on. |
knockbackUpForce | float | 0 | Upward lift added to the knockback so grounded targets pop loose instead of sliding. |
targetEffect | StatEffectDefinition | — | Stat effect applied to the target on every landed hit (a poison blade). |
instigatorEffect | StatEffectDefinition | — | Stat effect applied to this attacker on every landed hit (regen-on-hit). |
windupSeconds | float | 0.15 | Seconds between pressing Attack and the hitbox opening. |
activeSeconds | float | 0.25 | Seconds the hitbox stays open once the swing lands. |
cooldownSeconds | float | 0.6 | Seconds from one Attack press to the earliest next one. |
The CombatShowcase level pairs both target kinds with the same swing: punch the shipped Target Dummy (a DamageableObjectwearing the goblin visual, health bar included) and a plain crate side by side, then pick up the floor sword to see the attacker's own stat move the number rather than the swing itself.
Thrown props
ChargeThrowInteractable is the pickup-and-throw half: holding Attack while carrying charges a throw from a lob up to a full-power arc, shown live by a ghost trajectory line, and releasing launches the prop. ThrownDamageSource is the in-flight half: it watches its own impact speed and files a claim — attributed to whoever threw it, not to the prop — on any collision fast enough to count as a hit rather than a bump. The two halves are never wired up separately in practice — ChargeThrowInteractable requires a ThrownDamageSourceon the same object, and Unity adds one automatically if it's missing.
A Building placeable can opt into this same stack via its Throwable When Placed flag, so a crate placed with the hammer can also be picked up and thrown.
| Field | Type | Default | What it does |
|---|---|---|---|
carryDistance | float | 1.4 | How far in front of the thrower the prop is held. |
carryHeight | float | 1.2 | How high off the thrower's feet the prop is held. |
carryFollow | float | 18 | How snappily the prop follows the carry point — higher is stiffer. |
minThrowSpeed | float | 6 | Throw speed at zero charge — a tap-and-release lob. |
maxThrowSpeed | float | 18 | Throw speed at full charge. Power caps here, never past it. |
chargeSeconds | float | 1.2 | Seconds of holding Attack for power to sweep 0 → full, then ping-pong back down. |
upwardArc | float | 0.35 | Upward fraction added to the aim direction — 0 is flat, 0.5 is a steep lob. |
| Field | Type | Default | What it does |
|---|---|---|---|
damage | float | 20 | Points of damage per qualifying impact. |
affectedGroups | CharacterGroupMask | Everyone | Which sides this object may hurt — a flying crate doesn't take sides by default. |
canHitInstigator | bool | false | May the prop hurt whoever threw it? Off by default. |
knockbackForce | float | 0 | Knockback impulse along the prop's flight direction. |
knockbackUpForce | float | 0 | Upward lift added to the knockback. |
targetEffect | StatEffectDefinition | — | Stat effect applied to the target on every qualifying impact. |
instigatorEffect | StatEffectDefinition | — | Stat effect applied to the thrower on every qualifying impact. |
minImpactSpeed | float | 4 | Impacts slower than this are harmless bumps, not hits. |
restSpeed | float | 0.5 | Speed below which the prop counts as coming to rest. |
restSeconds | float | 0.5 | Seconds below rest speed before the prop disarms and drops its instigator. |
The CombatShowcaselevel's Payload Zone lines up three pads whose only difference is what their damage carries — pure knockback, an effect, or nothing at all — so the payload half of a claim is easy to see in isolation from the damage half.
Multiplayer: server arbitrates, owner applies
CombatReplicatorplays the equivalent role for the arbiter itself (routing a client's claim to the server), and ThrownReplicatorkeeps a thrown prop's instigator and flight state in agreement across machines. All three are installed automatically — they never appear in the Add Component search, so a half-wired network stack can't be assembled by hand. See the Networking Dashboardto check whether a given player's combat rows are installed and, in Play mode, live.
Combat without code
The same arbiter backs three low-code nodes, so a hazard volume or a scripted trap deals damage through the identical pipeline a weapon hit does — armour, invulnerability and death all apply the same way. Apply Damage (an instruction) deals damage with the same amount/mask/knockback/effect shape as a claim; Launch gives an object a velocity change with an arc, mass-independent; Took Damage(a condition) watches a character's replicated health and passes for a configurable window after it drops — the one combat hook that fires identically on every peer, unlike the code-level CharacterDamageable.OnDamaged event, which only fires on the machine that resolved the claim. Full field-by-field reference lives on Stock Conditions & Instructions.
Worked example: a damage source of your own
Nothing about a damage source is special-cased — a hazard, a spike trap, a custom projectile all follow the same two steps: resolve a target, then file a claim.
Resolve the target
DamageTargets.Resolve(collider.gameObject)walks up from any part (a limb collider, a child mesh) to theIDamageableon the whole object, or returns null — scenery walking into your volume is normal, not an error.File a claim
Build aDamageClaimand hand it toCombatArbiter.Active.FileClaim. The arbiter validates and, if accepted, delivers it — your code never callsApplyDamagedirectly.
void OnTriggerEnter(Collider other)
{
IDamageable target = DamageTargets.Resolve(other.gameObject);
if (target == null) return;
CombatArbiter.Active.FileClaim(new DamageClaim(
target, amount: 10f, instigator: gameObject,
CharacterGroupMask.Everyone, canHitInstigator: false));
}