Combat Contracts¶
namespace MassiveSwarmSystem.Runtime.Combat
Three small interfaces connect the swarm's attack layer to your game objects. Implement them on your own health and target scripts when you don't want the ready-made sample components. The swarm resolves them once when a target registers, then calls into whatever it found when a hit lands. There is no global manager and no physics-event dependency.
You may not have to implement anything
The sample components (PlayerCombatTarget, DestructibleCombatTarget,
SwarmAgentCombatTarget) already implement these contracts. Reach for the raw interfaces
only when you have an existing health system the swarm should call into. For the components,
their Inspector fields, and the turret/projectile workflow, see
Combat Integration.
How the swarm finds your contract¶
When a SwarmTarget registers, the manager resolves the contracts once
and caches them on the target:
GetComponentInParent<IDamageable>(): the receiver of swarm contact damage.-
GetComponentInParent<ITargetable>(): the swarm's own damage gate. Each step, before an agent swings or lands a contact hit, the manager checks the cachedIsTargetableand skips the target when it readsfalse. Weapons do not use this cache:ProjectileWeaponresolvesITargetableitself on whatever collider it hits. -
Your component must sit on the
SwarmTarget's GameObject or a parent, never a child. - The lookup runs at registration (
OnEnable). If you add the component after the target is already registered, disable and re-enable theSwarmTargetso the swarm picks it up. CallingRegisterTargeta second time does nothing: it sees the transform is already registered and returns before it re-reads the contracts. From script,UnregisterTargetthenRegisterTarget.
Swarm contact damage always calls the context overload
Every swarm hit carries a hit point and a direction, so there is always a context to pass, and
the melee/contact path calls ApplyDamage(float, in SwarmDamageContext) and never the plain
overload. Both are required members of IDamageable, so you cannot miss it by accident. If your
receiver does not care about the metadata, forward the context overload to the plain one and you
are done.
Pushback is the part of the context that varies. Swings ship with Swing Pushback Strength
at 1, so HasPushback is true, but Contact Pushback Strength ships at 0 on purpose, because
a shove on every tick reads as jitter. Handle a context whose HasPushback is false rather than
assuming a shove.
DamageableBase is a sample shortcut, not part of the runtime
The StarterKit ships DamageableBase, which implements IDamageable for you along
with a health pool and a death event. Subclass it while you are prototyping. It lives in the
sample assembly, so it disappears when you delete the StarterKit, and anything shipping in your
own game should implement the interface directly. See
Samples & Cleanup.
The damage call itself happens on the main thread inside the manager's FixedUpdate. The swarm
builds the context from the attacking agent's pose:
// Paraphrased from SwarmManager's attack pass, shown so you know what your
// ApplyDamage override receives.
SwarmDamageContext ctx = SwarmDamageContext.FromHit(
source: manager, // the SwarmManager that owns the agent
hitPoint: agentPos,
hitDirection: agentForward,
hitNormal: -agentForward,
pushbackStrength: attackProfilePushback);
damageable.ApplyDamage(damage, ctx);
IDamageable¶
public interface IDamageable
{
bool CanTakeDamage { get; }
bool ApplyDamage(float damage);
bool ApplyDamage(float damage, in SwarmDamageContext context);
}
CanTakeDamage is checked before every hit. Return false for i-frames, a dead state, or a
not-yet-spawned object. ApplyDamage returns true when damage was actually applied and false
when ignored.
Implementing the context overload on your own health component, with knockback straight from the hit data:
using MassiveSwarmSystem.Runtime.Combat;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerHealth : MonoBehaviour, IDamageable
{
[SerializeField] private float m_health = 100f;
private CharacterController m_controller;
private void Awake() => m_controller = GetComponent<CharacterController>();
public bool CanTakeDamage => m_health > 0f;
// Plain overload: the swarm never calls this one, but the projectile path and
// your own code might, so forward it to keep one code path.
public bool ApplyDamage(float damage) => ApplyDamage(damage, SwarmDamageContext.None);
public bool ApplyDamage(float damage, in SwarmDamageContext context)
{
if (!CanTakeDamage || damage <= 0f)
return false;
m_health -= damage;
if (context.HasPushback)
{
Vector3 push = context.GetPushbackDirection(transform.position) * context.PushbackStrength;
m_controller.Move(push * Time.fixedDeltaTime);
}
return true;
}
}
| Member | Description |
|---|---|
IDamageable.CanTakeDamage |
Gate checked before each hit. false makes the object immune right now. |
IDamageable.ApplyDamage(float) |
Applies damage with no hit metadata. Returns whether it landed. |
IDamageable.ApplyDamage(float, in SwarmDamageContext) |
Same, plus hit point / direction / pushback. The overload the swarm always calls. |
SwarmDamageContext¶
A small struct describing how a hit happened. Read the Has* guard before reading the matching
field, since an empty context (SwarmDamageContext.None) has them all false.
| Member | Type | Description |
|---|---|---|
Source |
UnityEngine.Object |
What caused the damage (the SwarmManager for contact hits, your weapon for projectiles). |
HitPoint / HasHitPoint |
Vector3 / bool |
World-space impact position. |
HitDirection / HasHitDirection |
Vector3 / bool |
Normalized direction the hit travelled. |
HitNormal / HasHitNormal |
Vector3 / bool |
Surface normal at the impact. |
PushbackStrength / HasPushback |
float / bool |
Push magnitude from the attack profile; HasPushback is true when it's positive. |
HasSource |
bool |
true when Source is set. |
None |
SwarmDamageContext |
A shared empty context: all guards false. |
FromHit(source, hitPoint, hitDirection, hitNormal, pushbackStrength = 0) |
SwarmDamageContext |
Builds a populated context, normalizing the directions for you. |
GetPushbackDirection(receiverPosition, flattenToXZPlane = true) |
Vector3 |
Resolves a push direction from whatever data is present: HitDirection, else receiver-minus-HitPoint, else -HitNormal. Flattens to XZ by default so receivers don't launch upward. Returns Vector3.zero if nothing usable. |
SwarmHitPoint¶
public static Vector3 Resolve(in RaycastHit hit, Vector3 referencePoint);
public static Vector3 OnCollider(Collider hitCollider, Vector3 referencePoint);
Resolves a usable contact point for the HitPoint above. Run every cast hit through it before it
reaches a context.
The reason is a Unity behavior that this asset walks into constantly: a cast that begins already
overlapping a collider returns distance 0, point Vector3.zero, and a normal that just
mirrors the sweep direction. Feed that straight into FromHit and the impact VFX and floating
damage number appear at the world origin. A melee arc inside a packed crowd starts overlapped on
most frames, so this is the common case here rather than a corner one.
Resolve keeps hit.point for a genuine sweep and, for the zero-distance case, falls back to the
closest point on the collider to referencePoint (pass the sweep origin). OnCollider is that
fallback on its own, for overlap queries that have a Collider but no RaycastHit. Box, sphere,
capsule and convex mesh colliders answer exactly; concave meshes and terrain echo the input back
from Collider.ClosestPoint, so those degrade to the collider bounds and finally to its transform
position.
Both SwarmProjectile and RotatingWeapon call it, so copying either one carries the fix along.
ITargetable¶
public interface ITargetable
{
bool IsTargetable { get; }
Vector3 TargetPoint { get; }
Transform AimTransform { get; }
}
Implement this to make an object a valid auto-target for ProjectileWeapon and any custom
targeting you write.
| Member | Description |
|---|---|
IsTargetable |
Whether this object is in play right now. Return false when it is dead or otherwise out. It gates two things: whether a weapon may pick it, and whether swarm swings and contact hits land on it. Note that it does not gate target selection, so agents still walk toward a target that reports false, they just cannot hurt it. |
TargetPoint |
World-space point a weapon aims at: the body center, not the pivot, for a character. |
AimTransform |
The transform used to resolve hierarchy ownership and avoid self-hits. |
Body radius lives on SwarmTarget, not here
ITargetable only governs auto-targeting. How wide a target's body is, meaning what the swarm
surrounds and stops around, comes from the SwarmTarget component on the
same object. The two are independent.
Quick reference in your IDE
Key members carry a short XML summary, so hovering them in Visual Studio or Rider shows a one-line description while you code. This page has the full detail.
Related¶
- Combat Integration: the ready-made components, their Inspector fields, and the turret/projectile workflow.
- SwarmTarget: registering a target and where the body radius comes from.
- Attack Profile: what drives
damageandPushbackStrength, and how reach is measured.