Skip to content

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<IDamageableWithContext>() — the receiver of swarm contact damage.
  • GetComponentInParent<ITargetable>() — the auto-target gate for turrets and projectiles.

  • 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, re-enable the SwarmTarget (or re-register the transform) so the swarm picks it up.

Swarm contact damage needs IDamageableWithContext, not plain IDamageable

The melee/contact path caches IDamageableWithContext specifically — every swarm hit carries a hit point, direction, and pushback, so there is always a context. A component that implements only IDamageable will not take swarm contact damage. Plain IDamageable is enough only for the projectile and sample-helper path, which dispatches through SwarmDamageableExtensions.ApplyDamage (below). When in doubt, implement IDamageableWithContext (or subclass DamageableBase, which already does).

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 / IDamageableWithContext

public interface IDamageable
{
    bool CanTakeDamage { get; }
    bool ApplyDamage(float damage);
}

public interface IDamageableWithContext : IDamageable
{
    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, IDamageableWithContext
{
    [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.
IDamageableWithContext.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 — 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.

SwarmDamageableExtensions

public static bool ApplyDamage(this IDamageable damageable, float damage, in SwarmDamageContext context);

A dispatch helper for the projectile and sample-helper path. It calls the context overload when the receiver implements IDamageableWithContext, otherwise the plain overload — and treats a null receiver as a no-op returning false. Call this from your own damage sources when you hold only an IDamageable reference but have a context to pass.

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 a weapon may pick this object right now. Return false when dead or out of play.
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 — 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.

  • 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 damage and PushbackStrength, and how reach is measured.