Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

BoxCollider2D.bounds Problem

Discussion in 'Scripting' started by Rob-Meade, Mar 27, 2019.

  1. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    Hi all,

    I'm running into some problems when trying to recalculate the bounds of child game objects and, despite looking at this for some time, and searching online, I've not found a way through the problem.

    If you consider a space invaders game, with a wall of invaders, this is effectively what I have. Each invader has a BoxCollider2D of it's own. In addition, they are all part of a formation gameobject, this has it's own BoxCollider2D.

    When the game is started the BoxCollider2D is resize using the bounds of the invaders BoxCollider2Ds, via the Encapsulate method. Each time an invader is shot, the same method is called to resize the formation's BoxCollider2D.

    When the game runs the formation's initial BoxCollider2D is sized perfectly. As you start to shoot the invaders in the right hand column it resizes slightly (not all of the sprites/colliders are the same size), which appears to be correct. The issue arises when the last invader in a column is hit. The formation's BoxCollider2D is resized fractionally, but not correctly. It takes an invader to be hit (doesn't matter which one) for it to then correct itself.

    This is causing me no end of problems as the BoxCollider2D on the formation is used for detection of playspace boundaries.

    I've tried to step through as best I can but cannot ascertain why the behaviour occurs.

    The code for the generation of the bounds as is follows;
    Code (CSharp):
    1.  
    2. private Bounds GetChildBoxColliderBounds()
    3. {
    4.     Bounds bounds = new Bounds();
    5.     BoxCollider2D boxCollider2D = null;
    6.  
    7.     if (_invaderFormation.Invaders.Count > 0)
    8.     {
    9.         bounds = _invaderFormation.Invaders[0].GetComponent<BoxCollider2D>().bounds;
    10.  
    11.         for (int i = 1, ni = _invaderFormation.Invaders.Count; i < ni; i++)
    12.         {
    13.             boxCollider2D = _invaderFormation.Invaders[i].GetComponent<BoxCollider2D>();
    14.  
    15.             if (boxCollider2D && boxCollider2D.enabled)
    16.             {
    17.                 bounds.Encapsulate(boxCollider2D.bounds);
    18.             }
    19.         }
    20.     }
    21.  
    22.     return bounds;
    23. }
    24.  
    This is based on an example I found online. In addition there are some checks to ensure that there are still invaders left in the formation, and that the BoxCollider2D component is enabled before its bounds are used. (I am turning off the BoxCollider2D when the invader is hit to allow any subsequent projectiles to travel through the death animation and hit the next invader etc).

    I should add that I am not specifically keen on line 9 above, I am unhappy with it not being just in the main `for` loop which follows, but when I've tried moving this, I tend to get a box twice the size of that which I require.

    I am also aware that this line doesn't get to have the check for whether the BoxCollider2D is enabled or not, its just assumed, which I don't like, and it could be that that is the problem, e.g. basing the size of the bounds on a collider which it shouldn't and then growing it further using the remaining (which are checked). I'm not entirely sure how to get around that though, as this seems to require at least one to kickstart the bounds before any further calls to Encapsulate.

    The above method returns the bounds to this;

    Code (CSharp):
    1.  
    2. private void ResizeBoxCollider()
    3. {
    4.     Bounds boxColliderBounds = GetChildBoxColliderBounds();
    5.  
    6.     _boxCollider2D.size = boxColliderBounds.size;
    7.     _boxCollider2D.offset = boxColliderBounds.center - transform.position;
    8. }
    9.  
    As the formation is moving, I apply the offset to take in to account the position of the formation itself.

    Here's a video to show the problem in detail;


    Any help/suggestions would be really appreciated, I've been going round in circles with this for some time now and would really like to be able to move passed it!

    Thank you in advance for your time/help :)

    Rob
     
    Last edited: Mar 27, 2019
  2. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Looks like you're calculating the bounds before removing killed invader from the array. Ensure that guy is out before the call to ResizeBoxCollider or, as an alternative, add alive test inside your loop:

    Code (CSharp):
    1.         for (int i = 1, ni = _invaderFormation.Invaders.Count; i < ni; i++)
    2.         {
    3.      
    4.             // i mean this line
    5.             if(_invaderFormation.Invaders[i].isDead) return;
    6.  
    7.             boxCollider2D = _invaderFormation.Invaders[i].GetComponent<BoxCollider2D>();
    8.             if (boxCollider2D && boxCollider2D.enabled)
    9.             {
    10.                 bounds.Encapsulate(boxCollider2D.bounds);
    11.             }
    12.         }
     
    Rob-Meade likes this.
  3. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    Hi,

    Thanks for such a prompt reply. That is correct, however the invader is removed from the array after. I have two events which are raised, one when the invader his hit, which then triggers the above resizing to occur, and also to change the state of an animator. This then runs the invader's death animation, at the end of that an animation event calls a method which then removes the invader from the array. This is why I used the `.enabled` property of the BoxCollider2D for each invader in the above check, if it isn't enabled, I assume that invader has been hit and is therefore on it's way to being destroyed.

    So, whilst the `for` loop effectively has one additional iteration, within that, the `if` statement should exclude that specific invader if the BoxCollider2D component is disabled.
     
    Last edited: Mar 27, 2019
  4. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Are you disabling the box collider before calling the hit event or after it? Bounds.Encapsulate is quite simple method and is hard to fail. Obviously the last invader was added to the formation bounds despite that .enabled property check. You may visualuize your calculations with Debug.Draw:

    Code (CSharp):
    1.         public static void DrawBounds(Vector3 min, Vector3 max, Color color, float duration = 3) {
    2.             var A = min;
    3.             var B = new Vector3(min.x, max.y);
    4.             var C = max;
    5.             var D = new Vector3(max.x, min.y);
    6.        
    7.             Debug.DrawLine(A, B, color, duration);
    8.             Debug.DrawLine(B, C, color, duration);
    9.             Debug.DrawLine(C, D, color, duration);
    10.             Debug.DrawLine(D, A, color, duration);
    11.         }
    12.  
    Code (CSharp):
    1.         for (int i = 1, ni = _invaderFormation.Invaders.Count; i < ni; i++)
    2.         {
    3.    
    4.             boxCollider2D = _invaderFormation.Invaders[i].GetComponent<BoxCollider2D>();
    5.             if (boxCollider2D && boxCollider2D.enabled)
    6.             {
    7.                 bounds.Encapsulate(boxCollider2D.bounds);
    8.                 DrawBounds(boxCollider2D.bounds, Color.yellow);
    9.             }
    10.         }
    This will highlight all boxes used to calculate formation rectangle for 3 seconds in the scene window.
     
  5. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    Hi,

    Thanks again for the reply and help :)

    The sequence is as follows;

    OnTriggerEnter2D called when projectile hits invader
    OnTriggerEnter2D sets state for Animator (to run death animation), sets `.enabled = false` for the individual box collider, then raises the Hit event

    It is at this point that the recalculation of the formation BoxCollider2D takes place.

    I've given the above a go, thanks for that, I got this though;
    upload_2019-3-27_13-9-55.png

    I changed the duration down to 1f and then get this;
    upload_2019-3-27_13-11-55.png

    I tried to be quick on the pause button as the yellow highlight obviously doesn't move with the actual sprites. The thin green line are the BoxCollider2Ds, in the above you can see that the formation's BoxCollider2D hasn't shrunk, despite the yellow highlights indicating it was the remaining 42 invaders that were used for the calculation.

    If I change the events so that the recalculation occurs when the Invader is actually destroyed the formation's BoxCollider2D appears to resize correctly. This is where I got to the other day and considered whether, perhaps, the bounds functionality doesn't care whether the BoxCollider2D is actually enabled or not, my plan was to then remove the BoxCollider2D component entirely from the individual invader when it was hit so that there would be no way at all it should be considered in any calculation. Unfortunately, this then led to a cascade of issues, where the component was expected and had then been removed.

    Using what I have is almost there, but I really don't understand why it take another invader to be hit before it resizes correctly from the previous resize, it's like its "out by one" so to speak, which is what made me consider that line outside of the `for` loop;

    Code (CSharp):
    1. bounds = _invaderFormation.Invaders[0].GetComponent<BoxCollider2D>().bounds;
    As this is the only line which utterly ignores the `.enabled` property, but at the same time, I am not sure how I can tell whether the invader at position 0 in the array is actually even in this first column at all. I've output its name and get `Invader_01` but that could potentially be any of the bottom row. Come to think of it, it shouldn't/couldn't be the one at the bottom of that first row, because all of the invaders above it have been destroyed at that point, so being that it would be the first to go, there should be no way at all that it is still lingering, and more than one frame would have passed.

    As you say, the Encapsulate method looks to be fairly straight forward, so I'm really struggling here to see what I've done wrong.
     
  6. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    One thing I've just tried - the highlighting code you offered, I've also made a separate call to the line above but using red, just to make it stand out;

    Code (CSharp):
    1.        
    2. if (_invaderFormation.Invaders.Count > 0)
    3.         {
    4.             bounds = _invaderFormation.Invaders[0].GetComponent<BoxCollider2D>().bounds;
    5.             DrawBounds(bounds.min, bounds.max, Color.red, 1f);
    6.  
    7.             for (int i = 1, ni = _invaderFormation.Invaders.Count; i < ni; i++)
    8.  
    9.             // ...
    10.  
    The red box appears once, at the very beginning and its in the bottom right hand corner. If I then shoot the invader in the bottom right hand corner, you don't see a red box. If I continue shooting the invaders above it in that first column you don't see a red box. If however I shoot any other invader, a red box appears and its always the bottom most invader on the far right.

    I'm not sure whether this means anything or not, I'm trying to digest it at the moment. I think though that would suggest that this line above, with the zero index, could be the culprit.

    It would perhaps also explain why, if I take all the invaders off of the bottom row, instead of shooting that first column, the formation's BoxCollider2D does resize as expected. It behaves correctly if you take a column off of the left also. It only misbehaves, it would seem, when its that right-most column that's being hit.


    (gotta be quick to spot the red boxes in this video!)
     
    Last edited: Mar 27, 2019
  7. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    To solve Invaders[0] issue, I suggest you rewrite your loop:

    Code (CSharp):
    1.         bool hasBounds = false;
    2.         Bounds result = default(Bounds
    3.  
    4.         for (int i = 1, ni = _invaderFormation.Invaders.Count; i < ni; i++)
    5.         {
    6.  
    7.             boxCollider2D = _invaderFormation.Invaders[i].GetComponent<BoxCollider2D>();
    8.             if (boxCollider2D && boxCollider2D.enabled)
    9.             {
    10.                 if(hasBounds) {
    11.                      bounds.Encapsulate(boxCollider2D.bounds);
    12.                 }
    13.                 else {
    14.                     bounds = boxCollider2D.bounds;
    15.                 }
    16.                 DrawBounds(boxCollider2D.bounds, Color.yellow);
    17.             }
    18.         }
    Also, don't try to be quick with pause button, use Debug.Break() in your code instead
     
    Rob-Meade likes this.
  8. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    Hi,

    Thanks, I'll give this ago, and also for the tip regarding Debug.Break().

    One thing, I did try moving that rogue statement within the `for` loop previously but when there wasn't a line which assigned a value to the bounds variable, e.g. it was only using the instantiation and then the Encapsulate method, I got a massive BoxCollider2D on the formation. I've not worked out why that was being that the instantiation would have been 0,0,0 for the bounds extents, it was the correct width, but weirdly double the height (plus a little bit). It was all quite odd which was why I left it as it was. I'll give the above a go though and see what happens - thank you :)

    Is there a bit of that second line missing? the `default(Bounds` bit?

    The above didn't work.

    I don't see a BoxCollider2D at all on the formation now, but if I look in the inspector this is because it is just tiny. I changed the `for` loop to begin at 0 as opposed to 1 also but this made no difference.

    I'm not sure how the bool is supposed to be working in the above as it isn't being set? Unless it was part of the default line which is partially missing, I think?

    ***BOOM***

    Working!!

    I added this;

    Code (CSharp):
    1. if (hasBounds)
    2. {
    3.     bounds.Encapsulate(boxCollider2D.bounds);
    4. }
    5. else
    6. {
    7.     bounds = boxCollider2D.bounds;
    8.     hasBounds = true;  // add this
    9. }
    ...and it appears to be working perfectly now, so it definitely looks like it was related to that zero index array item issue. Still don't understand why, but very pleased it's resolved!

    Thank you sooo much for you time, appreciating your help more than you can know! :)
     
    Last edited: Mar 27, 2019
  9. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    Of course, it should be
    Code (CSharp):
    1. default(Bounds);
    Bounds structure do not have empty value or flag. When you use it instantiated, but not initialized, it represents a rectangle at (0,0) with size of (0,0). If you encapsulate some invader at (200,300) with size of (30,20) into it, you get bounds at (0,0) having size (215,310) because it includes (0,0) that inital point. That's why you should initialize bounds with first alive object and only then you can safely encapsulate others.

    And I forgot one more thing

    Code (CSharp):
    1.                 else {
    2.                     bounds = boxCollider2D.bounds;
    3.                     hasBounds = true;
    4.                 }
    You need to set that hasBounds flag to true after initialization.
     
    Rob-Meade likes this.
  10. Rob-Meade

    Rob-Meade

    Joined:
    Oct 27, 2016
    Posts:
    56
    Thanks for the further info. I thought I tried
    Code (CSharp):
    1. default(Bounds) ;
    had assumed it was maybe just a bracket missing but it didn't seem to like it, will try again when I get back in.

    Thanks also for the explanation regarding the oversized BoxCollider2D when I just used the instantiated bounds, that makes so much sense. I did try the documention on the Unity websitr but it seemed a bit spares for this,so that explanation is really useful.

    So pleased I can move forward with the next thing now, this has been driving me nuts for days but I didn't want to ask for help u til I had at least tried a few things myself first.


    ***UPDATE***

    I added the default line again and it worked fine, not sure what happened the first time - all good though :)
     
    Last edited: Mar 27, 2019