Search Unity

Question OnTriggerEnter2D Won't Trigger Again Unless the Player Moves

Discussion in '2D' started by paultoth, Sep 22, 2022.

  1. paultoth

    paultoth

    Joined:
    Mar 30, 2021
    Posts:
    15
    Hello all,
    I am trying to use a toggleable Box Collider 2D as the weapon hitbox in a top down 2D game.The sword hitbox is off by default and then is activates when the player attacks for the duration of the animation, before being disabled again at the end.

    I am trying to use On Trigger Enter 2D to handle the sword hit detection, however I have run into a problem here. On the first attack everything works fine, the sword's box collider turns on, deals damage to the enemy, and is disabled again as it should be. If I attack a second time without moving though, the collider enables and disables correctly, but the OnTriggerEnter2D function is not called (I checked with debug log). If I then move my character around a little the OnTriggerEnter2D function again works like normal, until I try to attack twice without moving, where it stops detecting again.

    My question is: How can I get OnTriggerEnter2D to detect my sword Box Collider and the enemy collider every time, without having to move around?

    I tried OnTriggerStay2D as it should detect whenever the colliders are inside one another, but it suffers from the same issue of not working twice while stationary.

    I've provided my SwordAttack script as well as my Player and Enemy controllers below. Hopefully the problem lies therein, but please let me know if there is additional material required to find the source of the bug!

    Thank you all very much!

    SwordAttack.cs
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class SwordAttack : MonoBehaviour
    6. {
    7.  
    8.     public int damage = 10;
    9.     public Collider2D swordCollider;
    10.     Vector2 rightAttackOffset;
    11.  
    12.     private void Start()
    13.     {
    14.         rightAttackOffset = transform.position;
    15.     }
    16.  
    17.     public void AttackRight()
    18.     {
    19.         print("attackRight");
    20.         swordCollider.enabled = true;
    21.         transform.localPosition = rightAttackOffset;
    22.     }
    23.  
    24.     public void AttackLeft()
    25.     {
    26.         print("attackLeft");
    27.         swordCollider.enabled = true;
    28.         transform.localPosition = new Vector2(rightAttackOffset.x * -1, rightAttackOffset.y);
    29.     }
    30.  
    31.     public void StopAttack()
    32.     {
    33.         swordCollider.enabled = false;
    34.     }
    35.  
    36.     private void OnTriggerEnter2D(Collider2D other)
    37.     {
    38.        
    39.         Debug.Log("SwordCollider hit!");
    40.         if (other.tag == "Enemy")
    41.         {
    42.             Enemy enemy = other.GetComponent<Enemy>();
    43.  
    44.             if (enemy != null)
    45.             {
    46.                 enemy.healthSystem.DealDamage(damage);
    47.             }
    48.         }
    49.  
    50.        
    51.     }
    52.  
    53. }
    54.  
    Enemy.cs
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Enemy : MonoBehaviour
    6. {
    7.     public HealthSystem healthSystem;
    8.     public ContactAttack contactAttack;
    9.     public CapsuleCollider2D enemyCollider;
    10.    
    11.  
    12.     private void Start()
    13.     {
    14.        
    15.     }
    16.  
    17.    
    18.     public void RemoveEnemy() {
    19.         // Called at end of death animation to remove sprite.
    20.         Destroy(gameObject);
    21.     }
    22.  
    23.     public void RemoveCollider() {
    24.         // Called as soon as death animation plays to stop accidental player hits.
    25.         enemyCollider.enabled = false;
    26.     }
    27. }
    28.  

    PlayerController.cs

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5.  
    6. public class PlayerController : MonoBehaviour
    7. {
    8.  
    9.     public float moveSpeed = 1f;
    10.     public float collisionOffset = 0.05f;
    11.     public ContactFilter2D movementFilter;
    12.     public SwordAttack swordAttack;
    13.     public HealthSystem healthSystem;
    14.     public GameManager gameManager;
    15.  
    16.     Vector2 movementInput;
    17.     SpriteRenderer spriteRenderer;
    18.     Rigidbody2D rb;
    19.     Animator animator;
    20.     List<RaycastHit2D> castCollisions = new List<RaycastHit2D>();
    21.  
    22.     bool canMove = true;
    23.  
    24.     // Start is called before the first frame update
    25.     void Start()
    26.     {
    27.         rb = GetComponent<Rigidbody2D>();
    28.         animator = GetComponent<Animator>();
    29.         spriteRenderer = GetComponent<SpriteRenderer>();
    30.     }
    31.  
    32.     private void Update()
    33.     {
    34.  
    35.     }
    36.  
    37.     private void FixedUpdate()
    38.     {
    39.         if (canMove)
    40.         {
    41.             // First check if we can move at all
    42.             if (movementInput != Vector2.zero)
    43.             {
    44.                 // Then check if we can move in the xy vector we want, and move if it is free
    45.                 bool success = TryMove(movementInput);
    46.  
    47.                 // If the vector we want is obstructed...
    48.                 if (!success)
    49.                 {
    50.                     // First try to move in a straight X vector
    51.                     success = TryMove(new Vector2(movementInput.x, 0));
    52.  
    53.                     // And then try to move in a straight y vector
    54.                     if (!success)
    55.                     {
    56.                         success = TryMove(new Vector2(0, movementInput.y));
    57.                     }
    58.                 }
    59.  
    60.                 // Sets aminator flags to signal the switch between moving and attacking animations
    61.                 animator.SetBool("isMoving", success);
    62.             }
    63.             else
    64.             {
    65.                 animator.SetBool("isMoving", false);
    66.             }
    67.  
    68.             // Set direction of sprite to movement direction.
    69.             if (movementInput.x < 0)
    70.             {
    71.                 spriteRenderer.flipX = true;
    72.             }
    73.             else if (movementInput.x > 0)
    74.             {
    75.                 spriteRenderer.flipX = false;
    76.             }
    77.         }
    78.     }
    79.  
    80.     private bool TryMove(Vector2 direction)
    81.     {
    82.         if (direction != Vector2.zero)
    83.         {
    84.             int count = rb.Cast(
    85.                     direction,  // X and Y values between -1 and 1 that represent the direction from the body to look for collisions.
    86.                     movementFilter,  // The settings that determine where a collision can occur on, such as layers to collide with.
    87.                     castCollisions,  // List of collisions to store the found collisions into, after the cast is finished.
    88.                     moveSpeed * Time.fixedDeltaTime + collisionOffset);  // The amount to cast equal to the movement plus an offset. Can prevent getting stuck on terrain.
    89.  
    90.             if (count == 0)
    91.             {
    92.                 // Time.fixedDeltaTime runs physics interactions on a fixed timescale to keep things uniform if the frames aren't right.
    93.                 rb.MovePosition(rb.position + moveSpeed * Time.fixedDeltaTime * direction);
    94.                 return true;
    95.             }
    96.             else
    97.             {
    98.                 return false;
    99.             }
    100.         }
    101.         else
    102.         {
    103.             // Can't move if there's no direction to move in
    104.             return false;
    105.         }
    106.     }
    107.  
    108.     void OnMove(InputValue movementValue)
    109.     {
    110.         movementInput = movementValue.Get<Vector2>();
    111.     }
    112.  
    113.     void OnFire()
    114.     {
    115.         animator.SetTrigger("swordAttack");
    116.     }
    117.  
    118.     public void SwordAttack()
    119.     {
    120.         LockMovement();
    121.  
    122.         if (spriteRenderer.flipX == true){
    123.             swordAttack.AttackLeft();
    124.         } else {
    125.             swordAttack.AttackRight();
    126.         }
    127.     }
    128.  
    129.  
    130.     public void EndSwordAttack()
    131.     {
    132.         UnlockMovement();
    133.         swordAttack.StopAttack();
    134.     }
    135.  
    136.     public void LockMovement()
    137.     {
    138.         canMove = false;
    139.     }
    140.  
    141.     public void UnlockMovement()
    142.     {
    143.         canMove = true;
    144.     }
    145.  
    146.     public void PlayerDefeated() {
    147.         gameManager.Restart();
    148.     }
    149.  
    150. }
    151.  
     
  2. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    No idea what's going on in your project but triggers work fine. If you disable a collider, all its contacts are gone. If you enable it, the next contact will be an Enter. It doesn't need to move to cause an Enter.

    All I can really comment on is that you're modifying the Transform, something you shouldn't do with physics although we are forced to support it. When you do that, during the next simulation step when it's actually handled by physics (note, it doesn't change physics there and then), it'll see the Transform changed and be forced to recreate the collider from scratch each time.

    Also, the only thing that moves in physics is a Rigidbody2D. The physics system should be writing to the Transform system, not reading from it when they are changed. This should be an exceptional circumstance, not normal operation.

    If this "hit box" trigger needs to move then add a Kinematic Rigidbody2D to it and manipulate its position. Also, doing this you can turn off simulation for the Rigidbody2D which means it's taken out of the simulation along with all colliders/joints/contacts etc. It's still in memory though so it's much faster: https://docs.unity3d.com/ScriptReference/Rigidbody2D-simulated.html

    At the very worst (and I don't recommend ever modifying the Transform) you should set-up the transform BEFORE enabling any collider. That way, at least, means the collider is initially created at that position so:
    Code (CSharp):
    1. transform.localPosition = rightAttackOffset;
    2. swordCollider.enabled = true;
     
    Kurt-Dekker likes this.
  3. Clone3

    Clone3

    Joined:
    Jan 11, 2023
    Posts:
    1
    Hello! Maybe you have already found the solution to this problem. Anyway, I had the exact same problem. And it turned out that the solution was to set the enemy's RigidBody2D as "Dynamic" and Sleeping Mode to "Never Sleep".
     
    Last edited: Feb 7, 2023
    cthoma81 and TheTrueCreator like this.
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Which problem? Seems like you're discussing something else because keping the body awake has no impact on any Enter callbacks. The Stay callbacks are not produced for sleeping bodies but again, that's nothing to do with this thread.
     
  5. georgevl

    georgevl

    Joined:
    Nov 11, 2021
    Posts:
    2


    I believe this is the tutorial @paultoth I using is and it produces the same results for me. One hit, and then you need to move the player to receive a second one. Furthermore, @Clone3 proposes the use of Sleeping Mode: Never Sleep which in fact works but was hoping for a better solution.

    It may well be the tutorial somewhat messing up or something else not correctly setup, but I see the reported behavior too.
     
  6. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Looking at that video though, it's just using physics queries. Physics queries are not affected by sleeping. In-fact, nothing is affected by sleeping in terms of collision detection apart from the fact that, for performance reasons, we stop pushing OnCollisionStay2D/OnTriggerStay2D out when the respective body goes to sleep.

    The thing is, Ignoring the queries, for there to be a OnTriggerEnter2D, something needs to have been moved or created; both of these cause a body to wake up.

    I'd be happy to take a look if you could provide something I could look at.
     
  7. BaggerSmurf

    BaggerSmurf

    Joined:
    Apr 30, 2021
    Posts:
    2
    I'm running into the same thing, where I have:
    * A tree:
    - With a static Rigidbody2D
    - And a Collider (isTrigger = false)
    - And a script containing log calls in OnTriggerEnter/Exit2D
    * A player:
    - With a dynamic Rigidbody2D
    - And a Collider (isTrigger = false)
    - And a child object for the hitbox of an axe:
    o With a Collider (isTrigger = true) (initially disabled)


    When running up to the tree and enabling the hitbox collider followed by disabling the hitbox collider (swinging the axe), I see my log entries for OnTriggerEnter2D and OnTriggerExit2D on the tree object.

    However, when I don't move the character and press the key to enable/disable the hitbox (swinging the axe) again, OnTriggerEnter2D is not called.

    How should we set this up, such that we can keep hacking away at the tree (getting OnTriggerEnter2D calls) without moving the character, only enabling and disabling a trigger Collider?

    (I'm using Unity 2021.3.16f1.)
     
    Last edited: Mar 28, 2023
  8. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    As above, please send me an example of this happening.
     
  9. BaggerSmurf

    BaggerSmurf

    Joined:
    Apr 30, 2021
    Posts:
    2
    I've attached a minimal example project, like the one described in my previous post.

    1: Press E to activate the Tool's collider. Expected: trigger entered. Result: trigger not entered.
    2: Press A and D to move around a bit, then press E again. Expected: trigger entered. Result: trigger entered.
    3: Press E again, without moving in between. Expected: trigger entered. Result: trigger not entered.
    4: Repeatedly use the check mark in the editor to enable and disable the Tool's collider. Expected: trigger entered repeatedly. Result: trigger entered repeatedly.

    What needs to change about my setup to get repeated trigger entered when pressing E without moving?
     

    Attached Files:

    MelvMay likes this.
  10. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    So first, thank you for a reproduction case that clearly shows the issue.

    I've applied a fix that solves this issue and will also process backports for it.

    In the meantime, as a workaround, you should find that waking the Rigidbody2D using WakeUp when you enable will workaround this issue. The actual problem lies in how the Behaviour.enabled works with respect to contact updates but that's been fixed now.

    Alternately would be to not allow that Rigidbody2D to sleep using the Rigidbody2D Sleeping Mode of "Never Sleep".

    From your test script:
    Code (CSharp):
    1.         if (Input.GetKeyDown(KeyCode.E))
    2.         {
    3.             Debug.Log("Collider enabled");
    4.             _collider.enabled = true;
    5.             _collider.attachedRigidbody.WakeUp();
    6.         }
    7.  
     
    arachnoid_gg and BaggerSmurf like this.