Skip to content

Interactable Trap — Trigger Damage System

Designing effective trap defense for your Unturned base starts with understanding how InteractableTrap validates triggers through its five-layer protection chain, calculates per-entity-type damage, and resolves kill credit to the barricade owner. InteractableTrap extends InteractablePower and handles triggered area damage — both explosive (landmines) and contact-based (spike traps, barbed wire). It uses a child InteractableTrapTrigger collider for trigger detection, validates through a multi-layer protection system (setup delay, power requirement, self-exclusion, cooldown), and applies per-entity-type damage with kill credit resolved to the barricade owner.

Source code location: Unturned/Interactable/InteractableTrap.cs

Inheritance Chain

Interactable → InteractablePower → InteractableTrap

Trigger Detection

InteractableTrapTrigger

csharp
public class InteractableTrapTrigger : MonoBehaviour
{
    public InteractableTrap parentTrap;
    private void OnTriggerEnter(Collider other)
    {
        if (parentTrap != null)
            parentTrap.NotifyTrapEntered(other);
    }
}

A child trigger collider attached to the trap prefab. On any collider entering, it notifies the parent trap.

NotifyTrapEntered — Validation Chain

csharp
internal void NotifyTrapEntered(Collider other)
{
    if (other.isTrigger) return;                                    // Layer 1: Trigger guards
    if (Time.realtimeSinceStartup - lastActive < setupDelay) return; // Layer 2: Setup delay
    if (requiresPower && !isWired) return;                          // Layer 3: Power
    if (other.transform.IsChildOf(transform)) return;                // Layer 4: Self-exclusion
    if (Time.realtimeSinceStartup - lastTriggered < cooldown) return;// Layer 5: Cooldown

    // ... damage application
}

Validation Layers

LayerCheckPurpose
1other.isTriggerIgnore trigger colliders that shouldn't activate
2Setup delayPrevents immediate triggering after placement
3PowerUnpowered traps don't trigger
4Self-exclusionTrap cannot trigger itself
5CooldownMinimum interval between triggers

Timer Values

  • setupDelay (default 0.25s): Prevents the trap from damaging the player who placed it.
  • cooldown (default 0.0s): Minimum interval between triggers — configurable per trap asset.

Explosive Path

When isExplosive is true:

csharp
if (isExplosive)
{
    // Self-damage
    BarricadeManager.damage(transform, 5.0f, 1f, false, damageOrigin: EDamageOrigin.Trap_Wear_And_Tear);

    // Explosion
    ExplosionParameters explosionParameters = new ExplosionParameters(
        transform.position, range2, EDeathCause.LANDMINE, GetKillerId());
    explosionParameters.playerDamage = playerDamage;
    // ... set all damage types
    DamageTool.explode(explosionParameters, out kills);

    // Detonation effect
    EffectAsset detonationEffect = Assets.FindEffectAssetByGuidOrLegacyId(
        trapDetonationEffectGuid, explosion2);
    EffectManager.triggerEffect(parameters);
}

Killer ID Resolution

csharp
private CSteamID GetKillerId()
{
    Transform barricadeRoot = DamageTool.getBarricadeRootTransform(transform);
    if (barricadeRoot != null)
    {
        BarricadeDrop barricade = BarricadeDrop.FindByRootFast(barricadeRoot);
        if (barricade != null)
        {
            BarricadeData data = barricade.GetServersideData();
            if (data != null)
                return new CSteamID(data.owner);
        }
    }
    return CSteamID.Nil;
}

Kill credit goes to the barricade owner as resolved from serverside data. This ensures correct attribution even when the owner is offline.

Non-Explosive Path — Per-Entity Damage

EntityDamage PathSpecial
PlayerDamageTool.damage(player, SHRED, SPINE, ..., playerDamage)Breaks legs if isBroken
ZombieDamageZombieParameters(zombie, direction, zombieDamage)Hyper zombies deal 10 self-damage
AnimalDamageAnimalParameters(animal, direction, animalDamage)Standard animal damage

Leg Breaking

csharp
if (isBroken)
    player.life.breakLegs();

The isBroken flag on the trap asset causes leg bones to be broken, immobilizing the player.

Damage Tires

csharp
if (trapAsset.damageTires)
    transform.GetOrAddComponent[InteractableTrapDamageTires]();

A marker component that other systems (particularly VehicleManager) check to determine whether the trap can damage vehicle tires.

Power Requirement

As an InteractablePower subclass, traps require power from a nearby generator:

  • requiresPower reads from the trap asset.
  • isWired is set by the power system when a generator is in range.
  • When unpowered, NotifyTrapEntered returns at layer 3 without triggering.

Key Design Insights

  1. Five-layer protection — Setup delay, power, self-exclusion, cooldown, and trigger guard prevent accidental or exploit activation.
  2. Killer ID via barricade owner — Uses BarricadeDrop.FindByRootFast for O(1) ownership lookup.
  3. Per-entity damage paths — Different damage methods per entity type allow precise balance.
  4. Trap self-damage — Explosive traps take 5 damage per trigger, limiting their lifespan.

Worked Code Example: Custom Trap System

Proximity Warning Trap

csharp
using SDG.Unturned;
using UnityEngine;

public class ProximityWarningTrap : MonoBehaviour
{
    public InteractableTrap parentTrap;
    public float warningRadius = 15f;
    public float warningBeepInterval = 2f;
    private float _lastBeepTime;

    private void OnTriggerStay(Collider other)
    {
        if (parentTrap == null || !parentTrap.isWired)
            return;

        Player player = DamageTool.getPlayer(other.transform);
        if (player == null)
            return;

        float now = Time.realtimeSinceStartup;
        if (now - _lastBeepTime < warningBeepInterval)
            return;

        _lastBeepTime = now;

        ChatManager.serverSendMessage(
            "You are near an active trap zone.",
            Color.yellow,
            toPlayer: player,
            iconURL: null,
            useRichTextFormatting: true
        );

        EffectAsset warningEffect = Assets.FindEffectAssetByGuidOrLegacyId(
            default, 1 // pulse effect
        );

        if (warningEffect != null)
        {
            EffectManager.triggerEffect(
                new TriggerEffectParameters(warningEffect)
                {
                    position = transform.position,
                    relevantDistance = EffectManager.MEDIUM,
                    wasInstigatedByPlayer = false
                }
            );
        }
    }
}

Trap Network Manager

csharp
using SDG.Unturned;
using System.Collections.Generic;
using UnityEngine;

public class TrapNetworkManager
{
    private Dictionary[Transform, float] _lastTriggerTime = new Dictionary[Transform, float]();

    /// [summary]
    /// Registers a trap trigger event, enforcing a configurable global
    /// cooldown to prevent trap spam exploits.
    /// Returns true if the trap was allowed to trigger.
    /// [/summary]
    public bool TryTriggerTrap(Transform trapTransform, float globalCooldown)
    {
        float now = Time.realtimeSinceStartup;

        if (_lastTriggerTime.TryGetValue(trapTransform, out float lastTrigger))
        {
            if (now - lastTrigger < globalCooldown)
                return false;
        }

        _lastTriggerTime[trapTransform] = now;
        return true;
    }

    /// [summary]
    /// Calculates the effective damage a trap would deal to a specific
    /// entity type, accounting for the trap asset's multipliers and
    /// the entity's armor/resistance.
    /// [/summary]
    public static float GetEffectiveTrapDamage(
        ItemTrapAsset trapAsset,
        Player player,
        EDamageOrigin origin
    )
    {
        if (trapAsset == null || player == null)
            return 0f;

        float baseDamage = trapAsset.playerDamageMultiplier.damage;

        // Account for armor
        float armorReduction = 1f;
        if (player.clothing != null)
        {
            // Armor reduces damage proportionally
            int armorTier = 0;
            byte armorQuality = 0;

            if (player.clothing.hatAsset != null)
                armorTier = Mathf.Max(armorTier, (int)player.clothing.hatAsset.armorTier);

            if (player.clothing.vestAsset != null)
            {
                armorTier = Mathf.Max(armorTier, (int)player.clothing.vestAsset.armorTier);
                armorQuality = Mathf.Max(armorQuality, player.clothing.vestQuality);
            }

            armorReduction = 1f - (armorTier * 0.1f + armorQuality * 0.001f);
        }

        return baseDamage * Mathf.Max(armorReduction, 0.05f);
    }
}

Mermaid Diagram: Trap Trigger Validation Chain

Comparison: Trap Types and Damage Potential

FeatureExplosive TrapSpike/Barbed WireBarbed Wire FenceCaltrop
Base damage typeRadial explosionContact DOTContact DOTContact + tire
Self-damage5 per triggerNoneNoneNone
Ammo/consumableNo (unlimited triggers until destroyed)NoNoNo
Power requiredOptional (asset flag)OptionalOptionalNo
Entity targetsAll (explosion AoE)Player, zombie, animalPlayer, zombiePlayer, vehicle tires
Leg breakingNoYes (if isBroken)Yes (if isBroken)No
CooldownConfigurableConfigurableConfigurableConfigurable
Kill attributionBarricade ownerBarricade ownerBarricade ownerBarricade owner
Visual effectExplosion prefabNone (contact-based)NoneNone
Rangerange2 (explosion radius)Trigger collider radiusTrigger collider radiusVery small collider

Failure Modes and Common Mistakes

  1. Hyper zombie self-destruction chains — Hyper zombies deal 10 self-damage to the trap. If multiple traps are stacked, a hyper zombie entering the trigger zone sets off a chain reaction: Trap A damages the zombie (which is hyper), zombie deals 10 self-damage to Trap A, Trap B triggers on the same zombie, zombie deals 10 more self-damage. Five stacked traps results in 50 self-damage per zombie step — traps self-destruct faster than expected.

  2. Setup delay evaded by re-placement — The setupDelay (0.25s) uses lastActive timestamp, which resets when the barricade is placed again. Moving a trap (pick up + re-place) resets the setup delay, allowing the placer to re-enter the trigger zone immediately after placement without triggering. This can be exploited to bypass trap defense by rapidly repositioning.

  3. Explosive damage to friendlies — Traps do not differentiate between owner/group members and enemies. The DamageTool.explode call applies damage to ALL entities in range. An explosive trap placed in a shared base will damage teammates who walk over it. There is no friendly-fire exclusion in the trap code.

  4. Killer resolution failure on unowned traps — If a trap's barricade data is corrupted and the owner field is CSteamID.Nil, GetKillerId() returns CSteamID.Nil. Kill messages display as "[no name] killed [player]" and kill statistics are not attributed. This is common on servers that import barricade databases from external sources.

  5. Cooldown short-circuiting on rapid re-entry — A player can trigger a trap, take damage, exit the trigger zone, and re-enter within the cooldown window without triggering. This allows "trap running" — skilled players can dance through trap corridors by timing their entry/exit with the cooldown cycle.

How This Field Behaves Differently from the SDG Docs

  • SDG docs describe traps as "one-time use." The community wiki often characterizes traps like landmines as single-use items. In the SDK, explosive traps take 5 self-damage per trigger but survive multiple triggers if they have sufficient health. A landmine with 15 health (boosted via asset) can trigger 3 times before destruction.

  • SDG docs claim traps can be disarmed. Some documentation mentions a disarming interaction. In the SDK, traps have no disarm method. They can only be destroyed by damage (from explosions, zombies, or BarricadeManager.damage) or picked up by the owner if the barricade asset allows pickup.

  • SDG docs state trap damage is "per tick" for contact traps. The official documentation sometimes implies continuous damage over time for spike traps. In the SDK, NotifyTrapEntered fires once per trigger entry + cooldown cycle. Spike traps do not deal damage per frame — they damage once per valid trigger event.

  • SDG docs mention "pressure plate" trap variants. The community wiki discussion references pressure-sensitive traps that trigger under weight. In the SDK, all traps use the same OnTriggerEnter pattern. There is no weight threshold or mass-sensitive trigger. Any collider (player, zombie, animal, vehicle) triggers the trap.

Performance Considerations

Trigger Volume Overhead

Each trap with an active InteractableTrapTrigger adds a OnTriggerEnter Unity callback that fires on every collider entering its volume:

  • 1-50 traps: Negligible — Unity's physics system handles the overlap efficiently.
  • 50-200 traps: Each trigger sends an OnTriggerEnter event to the InteractableTrapTrigger component. The validation chain runs in O(1) per trigger. Acceptable.
  • 200+ traps in a dense area: Physics overlap checks may become expensive on the Unity physics thread. Consider reducing trigger collider sizes or consolidating multiple traps into fewer, larger traps.

Explosion Performance

Each explosive trap trigger calls DamageTool.explode(...), which performs:

  • A Physics.OverlapSphere to find all damageable entities in the blast radius.
  • Per-entity damage calculation and application.
  • Physics impulse (explosionLaunchSpeed) to nearby Rigidbody components.

A rapid sequence of trap explosions (e.g., a group of 10 zombies triggering 5 traps each) can generate 50 OverlapSphere calls in a single frame. This is acceptable on modern hardware but may cause stuttering on lower-end servers.

Memory

Trap state is minimal: a few floats (setupDelay, cooldown timer, lastTriggered timestamp) and a flag (isExplosive). Each trap uses approximately 32 bytes of agent data beyond its barricade state.

Deeper FAQ

Q: Can traps damage vehicles with passengers inside?

Yes. If a vehicle's collider enters the trap trigger, the trap checks for a DamageTires component. If present, tire damage is applied. If the trap is explosive, the explosion damages the vehicle (and by extension, passengers inside the vehicle). DamageTool.explode applies vehicle damage through vehicleDamageMultiplier in the standard explosion path.

Q: How is kill attribution handled for trap kills?

Kill credit goes to the CSteamID stored in the trap's barricade data owner field. If the owner is offline or has left the server, the kill is still attributed to their SteamID. If the barricade owner is CSteamID.Nil (0), the kill appears as from "an unknown source" in kill messages.

Q: Do traps respect safezone restrictions?

InteractableTrap does not check safezone flags in its trigger method. Traps in safezones will still deal damage. However, the BarricadeManager checks safezone placement rules when the trap is initially placed. A trap placed in a weapon-restricted safezone (safezone.noWeapons = true) is prevented at placement time, but if the safezone configuration changes after placement, existing traps are not retroactively removed.

Q: Can I set a trap to only trigger on zombies?

Not through vanilla configuration. The NotifyTrapEntered method does not filter by entity type. A Harmony patch on NotifyTrapEntered or on InteractableTrapTrigger.OnTriggerEnter is required to add entity-type filtering. Some server plugins implement "zombie-only" or "player-only" trap modes by checking the collider's parent component before calling the trap logic.

Q: What happens if two traps' triggers overlap?

Each trap fires independently. A single zombie entering the overlap of two traps will trigger both traps in the same frame (or on sequential physics updates). Both traps apply their full damage, and each trap receives its own self-damage. This can be intentionally exploited for "double mine" kill zones but creates unexpected damage spikes for players who assume one trigger = one trap.

Cross-References

Document history