Turning on a switch using OnTrigger[option]2D not behaving as expected

I joined a game jam recently and produced a nifty little game that involves the player character needing to turn on a 2D light via a light switch. The switch game object is setup as a trigger, and generally the game works well. However there is an issue whereby if the player enters the switch collider and stands still for a second or 2, the switch doesn’t respond or work. So I changed the method from OnTriggerEnter2D to OnTriggerStay2D and while performance seems better with this, the problem still persists. Before I post code (not at my dev machine right now) I just wanted to check if my logic around using the OnTrigger[×]2D is correct, if anyone else as experienced similair issues in their games, and if there are any obvious solutions or better ways to tackle this?

I don’t follow. You only get an OnTriggerEnter, OnTriggerEnter2D or OnCollisionEnter or OnCollisionEnter2D once when you first come into contact. It sounds like you’re expecting it to produce that continuously? That’s what the Stay callbacks are i.e. the same as Enter but Still touching so I don’t follow.

So the question is, can you define what you mean by “not behaving as expected”?

If you’re talking about “Stay”, those only get called when the Rigidbody2D is awake for performance reasons. If everything sent Stay all the time it’d be a nightmare for performance. This doesn’t affect “Enter” because to enter a contact you have to be moving anyway and therefore awake.

Yes so I realised this with the OnTriggerEnter2D in that I had to hit the switch instantly to get it to work. OnTriggerStay2D appears to be more forgiving by a second or so, but if I keep the player game object still for longer than this it also doesnt seem to respond.

The pseudocode is basically:

OnTriggerStay2D()
{
if (the key being pressed is enter && !SwitchAudioClip)
{
PlayTheSwitchSound;
TurnTheLightOn();
LightTurnedOnBool = true;
}
}

So the code is pretty simple. It just doesnt seem to fire after a short period of time even when using OnTriggerStay2D and I have no idea why not. Wondering if its a scene issue that I need to dissect. Does it go to sleep after a little while / can I extend the time its sending stay feedback, or is there just a better way to achieve this output that I dont know about? Im not a pro here - just an aspiring amateur.

I don’t follow what you mean here. By the look of your code you’re reading input during a physics callback but you should never do that. You should only read Input per-frame and flag whatever you need.

Forgiving? They are the same thing, called per simulation step which isn’t per-frame. They are called at the end of the simulation step which by default is during the fixed-update.

Yes, look at the Rigidbody2D Sleep Mode. When asleep, it won’t produce any Stay callbacks. You can force it to stay awake but I sense there’s a problem with how you’re reading input. Certainly don’t do that in callbacks.

Open the Rigidbody2D “Info” rollout. It shows you its current sleep mode. Also, the gizmos change colour for the colliders. By default they are configured to be a darker green than the lighter awake green but you can also change that in “Project Settings > Physics 2D > Gizmos”.

Thanks for the feedback and suggestions. Can you suggest a better way for me to achieve my goal? Like at a code level how do i need to change my approach to flag what I need, for example.

It’s standard practice that’s shown in pretty much every tutorial on basic physics movement. Esssentially read input during the “Update” callback and store that state. So for a “jump” you’d have a bool “IsJumping” field and read the input key for jump and set it true/false. Then later outside of the “Update” callback, perhaps in physics callbacks you’d use the “IsJumping” field and NOT read the input.

For example, the movement tutorial here shows the basics of disconnecting input from where it’s actually used:

None of that relates to sleeping though or even if that’s your problem here.

Thanks this has given me an idea of how I could handle this better with ontriggerenter and ontriggerexit - I dont think that sleeping is the problem, no. Time to experiment.

For this kind of thing I would approach it differently. Callbacks are good if you want to be notified when something suddenly happens and you then want to take action but in your case you don’t want it like this, you want it inverted i.e. you want to take action when the user selects Input to toggle the light switch and at that point see which switch, if any, you are touching and action it appropriately. You should use a physics query for this.

The best way to do this is respond when you check the input. You can then use one of many physics queries to determine this. Imagine your lights are on a “Light” layer and are all Static Trigger colliders. You can use the example script below to check if the Rigidbody2D (all its colliders) or even a specific Collider2D is overlapping anything on the “Light” layer using OverlapCollider. You also don’t need to set the Light layer to interact with the Player layer either as this is a query so you’re just asking for a specific layer. No callbacks required, doesn’t matter if it’s sleeping or not etc.

The Rigidbody2D here is the “Player”. It also naturally supports overlapping multiple light switches too (probably not that useful here but important to note) because it just asks what it’s overlapping. Here we just disable the collider but you could grab the light switch component and toggle it etc.

Here’s the code.

using System.Collections.Generic;
using UnityEngine;

public class LightChecker : MonoBehaviour
{
    Rigidbody2D m_Rigidbody;
    ContactFilter2D m_LightFilter;
    List<Collider2D> m_LightResults = new();

    void Start()
    {
        m_Rigidbody = GetComponent<Rigidbody2D>();

        // NOTE: We could make this field public and just configure it in the Inspector.
        // Much easier to do that!
        m_LightFilter.layerMask = LayerMask.GetMask("Light");
        m_LightFilter.useTriggers = true;
    }

    void Update()
    {
        // Did we hit space and were overlapping a light?
        if (Input.GetKeyDown(KeyCode.Space) &&
            m_Rigidbody.OverlapCollider(m_LightFilter, m_LightResults) > 0)
        {
            // Yes, so pretend to do something with them.
            for(var i = 0; i < m_LightResults.Count; ++i)
            {
                var lightCollider = m_LightResults[i];
                lightCollider.enabled = false;
            }
        }
    }
}

Hope this helps.

1 Like

Thanks so much for this feedback. This is a totally different approach to the one I was thinking of taking. I will experiment with this - thanks again!

1 Like