Skip to content

Component that gives any entity Trigger-like behaviour #1116

Description

@swoolcock

I am planning on implementing this myself

Yes

Describe your request

Essentially, the code modder can add a component to their entity that gives the OnEnter/OnStay/OnLeave functionality as if it were a Trigger. This means modders will no longer need to manually track CollideChecks across frames in the entity Update.

I've currently named the component Triggerable to be similar in naming to Holdable, and the actions OnEnter/OnStay/OnLeave, with their virtual invocation methods called InvokeOnEnter/InvokeOnStay/InvokeOnLeave in case the modder wants to override the component instead of assigning actions.

These actions (or method groups) can be passed into the constructor and/or assigned directly in the object initialiser. The first argument is an optional collider to use instead of the entity's Collider property.

Since colliders can be specified, this means multiple trigger regions can be assigned to the same entity if desired, each with different functionality.

This is an example where I've added three separate trigger regions to a gravity field, using different instantiation styles.

// custom hitbox, method groups
Add(new Triggerable(new Hitbox(16, Height, -16, 0), null, onStay, onLeave));

// custom hitbox, object initialiser
Add(new Triggerable(new Hitbox(16, Height, Width, 0)) {
    OnEnter = _ => Audio.Play(SFX.char_gran_laugh_firstphrase),
    OnLeave = player => player.Die(Vector2.Zero)
});

// default collider, object initialiser
Add(new Triggerable {
    OnEnter = player => player.SuperBounce(Top)
});

private void onStay(Player player) => player.Sprite.Scale.X = 2;
private void onLeave(Player player) => player.Sprite.Scale.X = 1;
triggerable.mp4

Additional context

No response

Activity

  1. self-assigned this
    on May 8, 2026
  2. microlith57 commented on May 8, 2026

    @microlith57
    Member

    peak

  3. microlith57 commented on May 8, 2026

    @microlith57
    Member

    also i do feel like the arbitrary "thing that triggers things" → "thing that gets triggered" mapping would be nice, but that's perhaps out of scope..
    thinking about something like "thing that triggers [custom touchswitch like thing]" component, triggering "triggerable [custom touchswitch like thing]" components

  4. swoolcock commented on May 9, 2026

    @swoolcock
    ContributorAuthor

    thinking about something like "thing that triggers [custom touchswitch like thing]" component

    I don't think this belongs as a component. Also the Triggerables are intentionally only updated as part of Player.orig_Update like normal Triggers, and I think they should probably stay contained that way.

  5. swoolcock commented on May 10, 2026

    @swoolcock
    ContributorAuthor

    @microlith57 not sure if this is what you're talking about (maybe I misunderstood you initially), but I was thinking it could definitely be done with entity type matching. I feel that TriggerZone might be a better name given you can pass it a collider and have any arbitrary number of them. We don't need separate source/target pairs since they pretty much do the same thing.

    class TriggerZone : Component {
        Action OnEnter... etc.
    }
    // triggered by any TEntity
    class EntityTriggerZone<TEntity> : TriggerZone where TEntity : Entity { }
    // triggered by any entity with TComponent
    class ComponentTriggerZone<TComponent> : TriggerZone where TComponent : Component { }
    // triggered by Player
    class PlayerTriggerZone : EntityTriggerZone<Player> { }
    // triggered by TheoCrystal, Glider, etc.
    class HoldableTriggerZone : ComponentTriggerZone<Holdable> { }

    For an entity that acts as the "target" (such as a spring, refill, etc.)

    // a better version of PlayerCollider
    mySpring.Add(new PlayerTriggerZone(optionalCollider));
    // a better version of HoldableCollider
    mySpring.Add(new HoldableTriggerZone(optionalCollider));

    For an entity that acts as the "source" (such as player, puffer, etc.)

    // if for some reason you wanted the player to enter/stay/leave anything with a Sprite
    player.Add(new ComponentTriggerZone<Sprite>(optionalCollider));
    // if for some reason you wanted the puffer to enter/stay/leave any subclass of Spikes
    puffer.Add(new EntityTriggerZone<Spikes>(optionalCollider));

    We would assume the TriggerZone collisions/triggering is done in TriggerZone.Update, which means that for any TriggerZones added directly to the Player entity, they would lag one frame behind calls to PlayerListener and Triggers since player movement happens after base.Update(). I believe the same applies to Puffer, TheoCrystal, and Glider. Not sure how we would handle this.

    Does this all sound too much?

  6. swoolcock commented on May 10, 2026

    @swoolcock
    ContributorAuthor

    As a side note, it would be nice to be able to pass "entity or component" as a constraint and not need to have two separate generic types. The only reason I care about the difference is whether I do Collide or CollideByComponent.

    Maybe we could add ITrackable to both Entity and Component, then add CollideByTrackable in the tracker that will just pick the right one. That would allow something like TriggerZone<T> where T : ITrackable.

    Edit: Given not all entities or components are trackable, we'd probably want something like ISceneItem.

  7. microlith57 commented on Jun 9, 2026

    @microlith57
    Member

    this is a good idea. i think the arbitrary source/target stuff is distinct enough to be a separate feature / pr
    i approve of ISceneItem also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions