Search Unity

CustomCollider2D.SetCustomShape

Discussion in '2D' started by KnuckleCracker, May 3, 2022.

  1. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    When calling CustomCollider2D.SetCustomShape (the NativeArray version), the colliders seem to update visually in the scene inspector. However, the colliders don't seem to collide until I do something like disable and reenable the gameobject holding the CustomCollider2D. It's as if Unity knows they are there, but box2d doesn't, but that's just a guess.

    When using SetCustomShapes (plural), everything works as expected. It seems that either SetCustomShape (singular) isn't updating everything internally, or there is some other API I'm supposed to be calling to force the refresh?

    This all comes up as part of an investigation on the best way to update a 'bunch' of colliders in a dynamic environment. Imagine, for example only, a large maze 128 on a side. The maze is constantly changing and the walls of the maze of made of squares, each a collider. The number of walls change over time and the walls that are 'active' also change.

    In one update model, SetCustomShapes is used and it's native arrays are populated in a job. The job scans what walls are visible (these walls are not gameobjects, just structs in another nativearray), and populates the PhysicsShape2Ds and vertices that SetCustomShapes uses. So if one wall changes, the whole bag of colliders gets rebuilt.

    The other model is to prepopulate colliders (128*128 of them in this example) and have them shoved off somewhere (way off at like -10000, -10000). They sit there and wait for walls to be added or removed. For each wall added/ removed, that collider (each is either just a circle or a square) is moved from it's -10000 spot to the correct spot in the maze. This is done with SetCustomShape (singular). Footnote; if the parked colliders at -10000 overlap, there is a large delay when the customcollider2d first updates. If the parked colliders don't overlap, there is no delay. I assume this has to do with the tree hashing in box2d, but that's just a guess.

    The second model doesn't work unless I force the CustomCollider2D to 'update' after any call to SetCustomShape. Disabling and enabling the CustomCollider2D is 'expensive'.

    So, is this a minor bug or am I missing a 'Refresh' API when using SetCustomShape?
    Secondly, what's the best model? Update everything or created parked colliders and bring them into and out of service using SetCustomShape (singular)?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Colliders do not collide (I know, seems counter-intuitive)... rather it is Rigidbodies (and Rigidbody2Ds) that collide.

    Rigidbodies only collide when they move or rotate, generally speaking. Rigidbodies left alone will soon fall asleep, unless kept awake.

    If you need an RB to notice that all of a sudden you have built a freeway straight through his yard, you need to generally get the RB to do an update.

    One easy way is to always tell a quiescent non-moving RB to go to their current location.

    Code (csharp):
    1. myRigidbody.MovePosition( myRigidbody.position);
    Obviously if you're moving the RB around you don't need to do that. But the act of doing the above SHOULD inform the RB that he needs to reconsider his position and the implications therein.

    I think you still run into trouble if you build a collider entirely AROUND an RB: only the edges of the collider are checked, generally speaking.
     
  3. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    In my example/test, there is a Rigidbody2D moving around the maze. It 'collides' fine with the CustomCollider2D walls so long as I use SetCustomShapes to create the CustomCollider2D physics shapes. If I use SetCustomShape (singular), it doesn't, unless I disable/enable the CustomCollider2D after calling SetCustomShape.

    --edit--
    I think I'm just fooling myself. Neither SetCustomShape nor SetCustomShapes seem to update the active colliders unless I disable and enable the containing Gameobject after any changes. If SetCustomShape(s) is called in Awake or Start, life if good. But updates made later update in the scene visually (the green collider lines move), but RigidBody2D's don't seem to interact with them.

    Note that it doesn't make a difference if I have a RigidBody2D (static) on the object with the CustomCollider2D. Also note that the kind of changes I am making is to the vertices of the colliders. They are all 4 vertex polygons (squares basically).
     
    Last edited: May 3, 2022
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    The shapes and their entry into the Box2D broadphase are there so they are ready to go instantly. We don't queue changes in 2D physics. You should also see an immediate shape-count in the inspector "Info" on the Collider2D itself. That's a count of shapes but more importantly, the gizmo drawing draws real physics shapes too. Are you seeing the gizmos drawing these?

    The profiler will you. It's like contacts being created or at least searched for but I have no details so I'm guessing.

    All shapes are created against a body, whether you have a Rigidbody2D or not, a body is involved because shapes are created against a body. Without a Rigidbody2D (as with all colliders and their shapes) they are created against the hidden static ground-body implicitly that lives at the world-origin with no rotation. This is just a convenience thing.

    This raises the point that a CustomCollider2D does not modify the shape geometry at all via Transforms. You are completely responsible for it. All shapes live in the space of the body they are created against. If this is the implicit ground-body then the shape local and world spaces are the same. If you have a local Rigidbody2D then the shapes are in the space of whatever that is.

    If you have a simple reproduction case you can share I'd love to take a look at it and see what's going on. If you can host it then DM me or if you like DM me with your email and I'll set-up a private place for you to upload to.

    EDIT: Just checking you're not using the 2022.2 alpha? There was a bug that was introduced (and quickly fixed) in the CustomCollider2D. That's most certainly fixed in the latest alpha.
     
  5. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    I've created a simple reproduction case that I think captures what I am seeing. It was created using a vanilla 2D URP Core project template in Unity 2021.3.1. I've attached the project, but it's as simple as updating a CC2D with the script below. The project just has a RigidBody2D sprite object with a CircleCollider2D attached. That object just 'falls' down the y axis under the influence of gravity. It will pass through the first piece of the collider geometry and then stop at the second piece. The gizmo in the scene view draws both of the collider rectangles correctly. It just appears that the first rectangle isn't 'live' until I disable and re-enable the CC2D. I've also attached a screenshot showing the circle resting on the lower rectangle after having passed through the first rectangle shape.
    CC2DCollider.png
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Collections;
    5.  
    6.  
    7. public class TestScript : MonoBehaviour {
    8.  
    9.     private CustomCollider2D cc2d;
    10.     private int updateCount;
    11.  
    12.     public void Awake() {
    13.         cc2d = GetComponent<CustomCollider2D>();
    14.     }
    15.  
    16.     //This is a contrived example used to demonstrate that subsequent updates to the CC2D vertices don't seem to completely update physics collisions.
    17.     //The scene view will show a green collider rectangle for where the collider should be, but the RigidBody2D circle in this project will not collide with it.
    18.     //If the CC2D is disabled/enabled after then vertices change, then the collision happens as expected.
    19.     //In the original test case, the vertices move around according to game logic and are updated in a Job with Burst, etc.  That has been removed here since it isn't necessary
    20.     //to reproduce the behavior.
    21.     public void FixedUpdate() {
    22.         if (updateCount == 0) {
    23.             var shapes = new NativeArray<PhysicsShape2D>(1, Allocator.Temp);
    24.             var vertices = new NativeArray<Vector2>(4, Allocator.Temp);
    25.             //This first update seems to apply properly.
    26.             //This can also be done in Awake or Start rather than on the first update.
    27.             shapes[0] = new PhysicsShape2D() { shapeType = PhysicsShapeType2D.Polygon, vertexCount = 4, vertexStartIndex = 0 };
    28.             vertices[0] = new Vector2(100, 100);
    29.             vertices[1] = new Vector2(100, 101);
    30.             vertices[2] = new Vector2(101, 101);
    31.             vertices[3] = new Vector2(101, 100);
    32.             cc2d.SetCustomShapes(shapes, vertices);
    33.         } else {
    34.             var shapes = new NativeArray<PhysicsShape2D>(2, Allocator.Temp);
    35.             var vertices = new NativeArray<Vector2>(8, Allocator.Temp);
    36.  
    37.             //Subsequent updates to the first 4 vertices don't seem to properly apply, not unless the cc2d is disabled and re-enabled.
    38.             //Note that the scene view shows the green collider at the propery location, the circle will not collide with it, however.
    39.             shapes[0] = new PhysicsShape2D() { shapeType = PhysicsShapeType2D.Polygon, vertexCount = 4, vertexStartIndex = 0 };
    40.             vertices[0] = new Vector2(-10, -10);
    41.             vertices[1] = new Vector2(-10, -9);
    42.             vertices[2] = new Vector2(10, -9);
    43.             vertices[3] = new Vector2(10, -10);
    44.  
    45.             //This geometry does update properly and the circle will collide with it as the circle falls. Presumably, subsequent updates
    46.             //to these vertices would not apply, same as with the first set of vertices above.
    47.             shapes[1] = new PhysicsShape2D() { shapeType = PhysicsShapeType2D.Polygon, vertexCount = 4, vertexStartIndex = 4 };
    48.             vertices[4] = new Vector2(-10, -15);
    49.             vertices[5] = new Vector2(-10, -14);
    50.             vertices[6] = new Vector2(10, -14);
    51.             vertices[7] = new Vector2(10, -15);
    52.             cc2d.SetCustomShapes(shapes, vertices);
    53.         }
    54.  
    55.  
    56.         //If the collider is disabled/enabled, then the changes to the vertices seem to apply and the circle starts colliding with CustomCollider2D again.
    57.         //So uncomment the following line and everything will work as expected.  THe ball will collider with the upper (first 4 vertices) part of the collider.
    58.         //cc2d.enabled = false; cc2d.enabled = true;
    59.  
    60.         updateCount++;
    61.     }
    62. }
     

    Attached Files:

    • CC2D.ZIP
      File size:
      76.5 KB
      Views:
      235
    MelvMay likes this.
  6. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    I've confirmed this as a problem or at least I don't see anything obvious you're doing that would cause this (don't think so) but I won't be able to dig into it until later today or perhaps tomorrow.

    It's up to you but if you want to submit a bug report with the project above attached and give me the case number, I fast-track it and take it over for the investigation. You'll then have something to track and I can give you any potential workarounds when I find the issue.

    Everything internal seems to be updating inside the collider, fixtures/shapes are updated along with the broadphase and any existing contacts are re-evaluated. It's very strange and I suspect a subtle issue. What's even stranger is that there's a unit-test that does pretty much what you're doing here by progressively adding an extra shape each time as well as ones that destroy shapes and just change shapes.
     
  7. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    Thanks for taking a look. I've created bug report and attached the CC2D.zip project to it. It is Case 1425289.
     
  8. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Just so you know, I've found the issue. CustomCollider2D does some low-level manipulation inside Box2D and even goes out of the way to make minimal changes so for existing shapes being replaced with the same shape geometry it'll do nothing. What's happening here is that it finds a change (the very first shape) but it's replaced with the same type of shape with the same number of vertices so it replaces those vertices, gets the normals calculated and flags it for new contacts. The problem is, it's missing updating its AABB in the broadphase so in your case, it's always the size of your original shape!

    This is why toggling the disabled state causes it to start working. A much quicker workaround is to add a Rigidbody2D (Static if you wish) and simply toggle the Rigidbody2D.simulated state. This doesn't destroy anything (unlike disabling the collider itself), it simply takes the shapes out of the broadphase then adds them back when you enable it so it's much quicker. You could also do disable this prior to udpating the collider and enabling it after if you wish.

    I've synchronized the AABB when you update a shape and it starts working. This is also a flaw in the existing test so I'll write a new set of tests to check the AABB is updating too.

    I can also post back here if you wish but I'll take over the case now and when the site updates (maybe an hour or so) you'll be able to track it via: https://issuetracker.unity3d.com/product/unity/issues/guid/1425289/

    Thanks for the report, I'll get the bug-fix into the release stream ASAP.
     
  9. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    Ah, interesting. Makes sense and not what I was guessing might cause it. Your explanation also points out something I have not considered, but seems notable and sensible... a CustomCollider2D creates a single AABB for itself, not one for each shape within the collider (Wrong, see below). Totally sensible, I'd just not thought about that and the performance impacts that might have for a CC2D with scattered shapes over a 'larger' area.
     
    Last edited: May 4, 2022
  10. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    It does create an AABB and entry in the broadphase for each shape. It was the specific shapes you were replacing, not creating that were the problem. Your initial shape was at 100,100 approximately. Setting shapes again and replacing this changed the shape but its AABB in the broadphase was not updated.

    In Box2D, each fixture/shape combo has an entry in the broadphase. A Unity collider is nothing more than a collection of separate fixture/shapes what we expose as PhysicsShape2D.

    Querying the broadphase will find a specific shape, not the whole "collider" then have to figure it out, that would be bad for performance. Box2D knows nothing about Unity colliders. :)

    In-fact, if you go into "Project Settings > Physics 2D > Gizmos" you can show these AABB.
     
  11. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    I also forgot to say sorry for the problem. It was a silly oversight on my part but I'll get it fixed ASAP. The workaround for now should serve you well though.
     
  12. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    It's all good... and CustomCollider2D is a great addition to Unity. Good to know about the shapes and AABB as well. I've updated my post above to not have incorrect information for people of the future that stumble across it.

    I had not noticed that about being able to turn on AABB gizmos. I've turned that on and I can definitely see different AABBs for each shape within a CC2D.
     
    MelvMay likes this.
  13. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
  14. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Just to let you know that the fix here has just been merged into: 2022.2.0a15, 2022.1.2f1 & 2021.3.4f1 so will be available when they're released.
     
    KnuckleCracker likes this.
  15. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    In a related problem/observation, I have noticed that if I do not call 'RigidBody2D.simulated=false' prior to making updates to a CustomCollider2D via SetCustomShapes(), then Physics2D.FindNewContactsTask takes longer and longer eventually grinding the whole game to a halt.

    Setting 'RigidBody2D.simulated=false' before updating the CustomCollider2D and then calling "'RigidBody2D.simulated=true' after the call to SetCustomShapes() skirts this problem. I don't know if this indicates an internal issue that can be improved, or if the docs need updating. But just calling SetCustomShapes (a lot, like every frame) on a CustomCollider2D with a RigidBody2D present on the same object will eventually grind the Physics2D sim to an effective halt.

    I'm using Unity 2022.2.0b13, and updating the CustomCollider2D every frame with roughly 100 shapes and 400 vertices, via the CustomCollider2D.SetCustomShapes(NativeArray<PhysicsShape2D> shapes, NativeArray<Vector2> vertices) API in this test.
     
    MelvMay likes this.
  16. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    To confirm, you shouldn't have to do this at all. Turning off simulation takes the shapes out of the broadphase and removes all contacts for all attached colliders. It doesn't alter how many shapes there are etc. Very strange. Assuming it's for the same reason, disabling/enabling the collider would do the same although there, that destroys/recreates the shapes hence it being slower.

    There's been no bug reports beyond yours above for this so do you have a simple reproduction case you could share as I'd love to take a look at it for you? If you cannot host it then feel free to DM me with your email and I'll set-up a private host for us to share the project. I would ask though that you strip it down to bare bones (as much as possible) before zipping up the project, delete the library folder too!

    In the meantime, I'll try to reproduce this locally this morning using the same API.

    Thanks!
     
  17. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    I tried a number of options. Here is what I determined:
    Requires Multithreading to be enabled in the Physics2D settings.
    Requires Unity 2022. I used 2022.2.0b13. Does not reproduce in Unity 2021.

    I've attached a project that reproduces the problem, but this is the only script. Note, this is a bespoke test case created from scratch to reproduce this problem.

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using Unity.Burst;
    8. using Unity.Collections;
    9.  
    10. public class TestScript : MonoBehaviour {
    11.  
    12.     private CustomCollider2D cc2d;
    13.     private Rigidbody2D rb2d;
    14.     private int len;
    15.  
    16.     private void Awake() {
    17.         cc2d = GetComponent<CustomCollider2D>();
    18.         rb2d = cc2d.attachedRigidbody;
    19.         len = 100;
    20.     }
    21.  
    22.     private void Update() {
    23.         //For testing. Geometry was originally created in a job but simplified here for this reproduction case.
    24.         NativeArray<PhysicsShape2D> shapes = new NativeArray<PhysicsShape2D>(len, Allocator.TempJob);
    25.         NativeArray<Vector2> vertices = new NativeArray<Vector2>(4*len, Allocator.TempJob);
    26.         for(int i = 0; i < len; i++) {
    27.             shapes[i] = new PhysicsShape2D { shapeType = PhysicsShapeType2D.Polygon, vertexCount = 4, vertexStartIndex = i * 4 };
    28.             vertices[i * 4] = new Vector2(i, 0);
    29.             vertices[i * 4 + 1] = new Vector2(i, 1);
    30.             vertices[i * 4 + 2] = new Vector2(i + 1, 1);
    31.             vertices[i * 4 + 3] = new Vector2(i + 1, 0);
    32.         }
    33.  
    34.         //Set to false then true after the update to skirt the problem.
    35.         //rb2d.simulated = false;
    36.         cc2d.SetCustomShapes(shapes, vertices);
    37.         //rb2d.simulated = true;
    38.  
    39.         shapes.Dispose();
    40.         vertices.Dispose();
    41.     }
    42.  
    43. }
    44.  
     

    Attached Files:

    MelvMay likes this.
  18. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Just to confirm I can reproduce this. Certainly, it's nothing specific to CustomCollider2D but it seems to have revealed something very strange going on in the multithreaded find contacts task. This is even more confusing because MT hasn't changed in a very long time.

    Investigating now.
     
    KnuckleCracker likes this.
  19. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    Found and fixed the problem. So yes, the fault is indeed in the MT contacts discovery task. Your test is triggering a condition where there are no new contacts so what ends up happening is that the queued "moves" (Box2D speak for something in the broadphase might have moved/changed) are not cleared. This means that they grow and grow without limited. This technically doesn't cause any issues apart form it just taking longer and longer each update.

    I can see how the change has happened now, it's an indirect change that only the MT code would see.

    I think your workaround is the only way to do so. Really sorry for this issue.

    I'm going to get a fix out for this immediately.
     
    KnuckleCracker likes this.
  20. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    So yes, with the tiny change it's now stable. Previously the time taken steadily increased.

    I'll do what I can to expedite this change and keep you informed on this thread.

    Again, sorry for such a silly oversight on my part.

    Stable.png
     
    KnuckleCracker likes this.
  21. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    Excellent and thanks for your rapid attention
     
    MelvMay likes this.
  22. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    I've got the fix and two backports done. I'll get it reviewed in the morning and QA'd this week then push it into the appropriate queues. I'll ping here when it lands and give you a version.
     
    KnuckleCracker likes this.
  23. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    So you know, FYI here's the references:

    D2D-4284 (2023.1)
    D2D-4285 (2022.2)
    D2D-4286 (2022.1)
    D2D-4288 (2021.3)
     
    KnuckleCracker likes this.
  24. redcurrantsjam

    redcurrantsjam

    Joined:
    Nov 21, 2022
    Posts:
    4
    For each shape, it does produce an AABB and entry in the broadphase. Not the shapes you were making, but the specific shapes you were replacing, were the issue. Your initial shape was somewhere around 100,100.
     
  25. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    I'm not following why you're replying to me. I understand what is going on here, I wrote it. :)
     
  26. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    Another anomaly with CustomCollider2D. Again, could just be me or a setting I have missed.

    A CustomCollider2D attached to a child object does not seem to be positioned properly, unless the child object has a RigidBody2D attached. This does not appear to be the case for any of the other Collider2Ds (like PolygonCollider2D).

    I've attached an image that shows a unit with a child offset from the origin. If I attach a RigidBody2d to the child and then move the child around, the CustomCollider2D follows the child as expected (and like all of the other Collider2Ds do). Without a RigidBody2D, the collider appears anchored at the origin regardless of the position of the child.

    I observed this after switching from a PolygonCollider2D to CustomCollider2D (which I'm determined to eventually harvest the performance advantages from).
     

    Attached Files:

  27. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    The CustomCollider2D is completely custom. It doesn't adopt the hidden and expensive Transform operations that slow other colliders down when they change. The shape vertices you put in are the ones that are used, period. Transforms won't affect them.

    All shapes in 2D physics (Box2D) are relative to a body (they are actually created via them). With a Rigidbody2D they are relative to that. Without a Rigidbody2D they are relative to the ground-body which lives are the world-origin. Without a Rigidbody2D you're essentially creating Static (non-moving) shapes.

    It's nothing to do with being a child. The only thing that moves in physics is a body. In the case above, the body changes position therefore anything created against it does. The shapes are not changing.

    This is why we say don't modify the Transform. When you do this on an implicitly static collider (its shapes created against the ground-body), they have to be completely recreated with all their geometry recalculated because their local-space is the same as the world-space. When you move a body, the local-space of the physic-shape doesn't need to change because it's in the space of the body.

    In short, the only transformation that the physics engine understands is a body moving.
     
  28. KnuckleCracker

    KnuckleCracker

    Joined:
    Dec 27, 2011
    Posts:
    81
    Ok, got it. So in the example image above, when using a custom collider, I need to offset and reapply the collider geometry if/when the child transform changes position relative to its parent. For 'fixed' position child transforms, this would be a one time thing.

    For confirmation; regarding rotations (and scale), it appears this is not necessary? The CustomCollider2D geometry appears to be tracking the child transform rotations and scale without having to reapply updated geometry to the CustomCollider2d. I can rotate a child transform around the z axis in the editor and the customcollider2d tracks the changes.
     
  29. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,500
    If you intend to move it, add a Rigidbody2D and set its position/rotation. You can make make it Static. Despite the popular myth, a Rigidbody2D is NOT expensive. Static has pretty much near zero cost. If you have a Rigidbody2D on the parent then you can use a Kinematic child which will track the parent position too and you'll be able to move that body independently should you wish.

    If you're changing the Transform, the collider will need to be recreated from scratch which can be very expensive given the fact that you can put an unlimited amount of shapes in the CustomCollider2D.

    Don't ever modify the Transform.
     
    KnuckleCracker likes this.