SwarmAgent¶
SwarmAgent is the thin MonoBehaviour that lives on every agent visual GameObject. Use it to apply knockback, trigger a hit flash, or read which simulation slot the visual currently occupies.
Overview¶
Each time the swarm spawns a new agent, it pulls a pooled GameObject from SwarmVisualManager, attaches a SwarmAgent to it (or reuses the existing one), and binds it to the owning SwarmManager and that agent's current simulation slot. When the agent dies or is recycled, the visual is released back to the pool and the component unbinds, and its Manager becomes null.
SwarmAgent is a temporary handle, not a persistent identity. The same component (on the same GameObject) will be bound to a completely different simulation slot after a despawn-and-respawn cycle.
Do not cache SwarmAgent references across frames
A reference you grabbed when an agent spawned becomes invalid the moment that agent despawns. The component silently unbinds, and its Manager becomes null. Always resolve the SwarmAgent in the same call that triggered your reaction (e.g. OnCollisionEnter, OnTriggerEnter, OnDamageTaken), then discard it. Do not store it in a field that outlives that call. When you do need to keep a reference to one agent over time, grab its Handle and act through SwarmManager. See Handle below.
Public API Reference¶
Properties¶
Manager¶
The SwarmManager that owns this agent's simulation slot. null when the visual is in the pool (unbound).
Use Manager when you need to call SwarmManager methods directly and have a SwarmAgent reference in hand. All force methods on SwarmAgent already check that Manager is non-null before forwarding.
Handle¶
A stable SwarmAgentHandle for this agent's simulation slot, safe to cache across despawns. Returns default (invalid) while the visual is pooled. Pass it to the per-agent SwarmManager methods (AddAgentForce, DespawnAgent, RelocateAgent, ...) instead of holding the SwarmAgent component. The handle keeps pointing at this agent as others despawn, and quietly stops resolving once this agent is gone.
AgentCollider¶
The collider that SwarmVisualManager either found on the prefab or created automatically at spawn time. Use this when you need the exact collider for physics queries.
A spawned agent always has one. If the prefab ships without a collider, the system adds a sphere, or a capsule when the renderer is meaningfully taller than it is wide. There is no setting to turn that off, so AgentCollider only returns null on a component that has never been bound to a slot.
Force Methods¶
All force methods return bool. true means the force was accepted and applied. false covers every case where nothing happened: the agent is unbound or its slot is invalid, the manager has not finished initializing or has no SwarmSettings assigned, the force contains a NaN or an infinity, or the force is too small to matter once flattened to the XZ plane. That last one catches both an under-threshold force and a force whose resulting velocity change rounds away against a high Push Resistance on the agent's archetype. A false return is silent; no exception is thrown.
AddForce¶
public bool AddForce(Vector3 force)
public bool AddForce(Vector3 force, ForceMode forceMode)
public bool AddForce(Vector3 force, ForceMode forceMode, float externalControlDuration)
Applies a continuous force to the agent, using Unity's standard ForceMode semantics (Force, Acceleration, Impulse, VelocityChange). The force is forwarded to SwarmManager.AddAgentForce.
One deviation from Unity physics: the Y component is thrown away before anything else runs. Agents move on the ground plane, so every force is flattened to XZ, and a purely vertical push does nothing at all and returns false. Explosions still read fine, just aim them outward instead of up.
externalControlDuration sets how many seconds the external push stays dominant. Dominant, not exclusive: while that timer runs the manager scales normal steering by External Force Steering Authority (SwarmSettings, 0.35 by default) rather than switching it off, so a knocked-back agent keeps leaning toward its target on the way out. Raise that value and knockback turns mushy; drop it to 0 and agents go completely limp for the duration. A negative value falls back to Default External Control Duration (0.45 seconds by default); 0 gives steering its full authority back immediately. The shorter overloads that omit the parameter behave like passing 0.
Use AddForce for sustained pushback (e.g. a wind zone applying force each frame) or when you want Unity physics force semantics.
AddImpulse¶
public bool AddImpulse(Vector3 impulseVelocity)
public bool AddImpulse(Vector3 impulseVelocity, float externalControlDuration)
Applies a one-shot velocity impulse for knockback from a single hit. The impulse matches AddForce(impulseVelocity, ForceMode.Impulse), with one difference in the default: omit externalControlDuration here and it falls back to the manager's Default External Control Duration (from SwarmSettings), whereas the no-duration AddForce overloads resume steering immediately.
externalControlDuration works exactly as it does on AddForce, including the shared steering authority described there: a negative value, or omitting it, uses the Default External Control Duration; 0 gives steering its full authority back immediately. The Y component of impulseVelocity is discarded too, so agents get pushed across the ground rather than launched.
Tint Methods¶
The tint system lets multiple simultaneous sources (damage flash, selection highlight, freeze effect) each own their own color contribution. The agent renders whichever source has the highest effective intensity (intensity × alpha). The stack holds up to four contributions; if full, the weakest is evicted.
Use a stable, unique string as sourceId per system or effect type (e.g. "damage", "freeze"). Reusing the same sourceId replaces that slot rather than adding a second entry.
PulseTint¶
public void PulseTint(string sourceId, Color color, float duration, AnimationCurve curve = null, bool useUnscaledTime = false)
Starts a tint that fades from full intensity to zero over duration seconds, then clears itself automatically. The fade is linear unless you supply an AnimationCurve, and the curve maps normalized time [0,1] to intensity [0,1].
useUnscaledTime: when true, the pulse animates even while Time.timeScale == 0 (e.g. during a pause menu). Defaults to false.
Calling PulseTint again with the same sourceId before the first pulse finishes restarts the pulse from full intensity.
SetTint¶
Sets a persistent tint contribution (no auto-fade). Call this every frame to drive an animated tint from your own curve or script. Clear it explicitly with ClearTint when done.
intensity is clamped to [0, 1].
ClearTint¶
Cancels any running PulseTint and clears the slot for that sourceId.
ClearAllTints¶
Cancels all running pulses and clears the entire tint stack. Called automatically when the agent is recycled, so no manual cleanup is needed on despawn.
TryGetResolvedTint¶
Returns the winning tint color and intensity, whichever source currently has the highest effective contribution. Returns false (and leaves outputs at defaults) when the stack is empty.
Use this when you have a secondary renderer (not managed by VAT) that needs to mirror the same tint envelope.
Scripting Recipes¶
Knockback on hit¶
An agent walks into a trigger volume on the player's weapon and gets shoved away from it.
using MassiveSwarmSystem.Runtime;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class KnockbackOnHit : MonoBehaviour
{
[SerializeField] private float m_impulseStrength = 8f;
[SerializeField] private float m_externalControlDuration = 0.3f;
private void Awake()
{
Rigidbody body = GetComponent<Rigidbody>();
body.isKinematic = true;
body.useGravity = false;
}
private void OnTriggerEnter(Collider other)
{
SwarmAgent agent = other.GetComponentInParent<SwarmAgent>();
if (agent == null)
{
return;
}
// Push away from the weapon, flattened to the XZ plane.
Vector3 direction = agent.transform.position - transform.position;
direction.y = 0f;
if (direction.sqrMagnitude < 0.0001f)
{
direction = transform.forward;
}
agent.AddImpulse(direction.normalized * m_impulseStrength, m_externalControlDuration);
}
}
The weapon needs the Rigidbody, not the agent
Agent visuals carry a collider and nothing else. No Rigidbody is ever added to them, so as far as PhysX is concerned they are static colliders, and two static colliders never talk to each other. Whatever object hosts this script has to bring the kinematic Rigidbody for the trigger to fire at all. That is the same rig the StarterKit's TouchDamageSource builds for itself at runtime.
GetComponentInParent rather than GetComponent: the collider that reports the overlap can sit on a child of the agent when the prefab ships its own.
Damage flash on hit¶
Play a quick red flash when an agent takes damage. The OnDamageTaken event is declared on DamageableBase, which SwarmAgentCombatTarget (see Combat Integration) inherits through CombatTargetBase. Subscribe to it and pulse a tint on the agent.
The StarterKit's own DamageFlashFeedback starts from the same event but then takes the other route described below: it animates the flash in its own update driver and pushes the current color through SetTint each frame, which is what lets one component own the flash timing for a whole swarm. PulseTint is the simpler option when you only need a fire-and-forget flash on one agent.
using MassiveSwarmSystem.Runtime;
using MassiveSwarmSystem.Samples.StarterKit;
using UnityEngine;
[RequireComponent(typeof(SwarmAgent))]
public sealed class AgentDamageFlash : MonoBehaviour
{
[SerializeField] private DamageableBase m_damageable; // the SwarmAgentCombatTarget on this agent
private SwarmAgent m_agent;
private void Awake() => m_agent = GetComponent<SwarmAgent>();
private void OnEnable()
{
if (m_damageable != null)
{
m_damageable.OnDamageTaken += HandleDamageTaken;
}
}
private void OnDisable()
{
if (m_damageable != null)
{
m_damageable.OnDamageTaken -= HandleDamageTaken;
}
}
private void HandleDamageTaken(float damage, SwarmDamageContext context)
{
m_agent.PulseTint("damage", Color.red, 0.2f);
}
}
This one needs the StarterKit
DamageableBase and CombatTargetBase live in MassiveSwarmSystem.Samples.StarterKit, the namespace that matches the assembly they compile into, so the using line above tells you the dependency up front. A script that references them stops compiling if you delete the StarterKit sample. Copy the two files into your own assembly if you want the damage contracts without the rest. The IDamageable and ITargetable interfaces are core and stay either way.
Practical Usage Guidance¶
SwarmAgent is the right tool when your code already has a reference to a specific visual GameObject, from a collision event, a raycast hit, or a damage callback on SwarmAgentCombatTarget. It forwards directly to the simulation without requiring you to manage agent indices manually.
For forces: AddImpulse is for single-hit knockback; AddForce is for sustained per-frame push (wind zones, explosion radius). Both respect SwarmSettings limits (MaxExternalVelocity, DefaultExternalControlDuration).
For tint: PulseTint handles one-shot flashes (damage, freeze, stun). Only reach for SetTint when you need to drive the tint value from your own per-frame logic.
Where SwarmAgent does not help: if you need to read simulation data in bulk (every agent's position for your own rendering or spatial queries), use the manager's span readers like ReadAgentPositions(). SwarmAgent is per-visual only and is not a batch API.
Where SwarmAgent is the wrong pattern: do not use it for observing agent state across multiple frames (health polling, position sampling, proximity checks). Hold a SwarmAgentHandle and read through the manager's handle getters, namely TryGetAgentPosition, TryGetAgentVelocity and the rest, instead.
Quickstart: typical hit-reaction setup
- Add
SwarmAgentCombatTargetto your agent prefab to make agents damageable. - In your damage handler, call
GetComponent<SwarmAgent>()on the hit GameObject. - Call
AddImpulseto push the agent away, thenPulseTintto flash it. - Done. You don't need to cache any reference: resolve and use in the same call.