What's the fastest alternative to PolygonCollider2D's SetPath() for many points?

I have a procedural thing I’m working on, and it requires a procedural mesh collider. For 2D, I was hoping to use the PolygonCollider2D simply give it a list of Vector2 points and define which goes to which shape myself. However, this does not seem to be an option in the API. The closest I’ve seen is just setting SetPath over and over, but this is EXTREMELY slow and generates a lot of garbage!

// slow af 
polygon2D.pathCount = _nativePolygonList.Length / 4;
               
var pointsIndexOffset = 0;
for(var i = 0; i < _nativePolygonList.Length / 4 - 1; ++i)
{
    polygon2D.SetPath(i, new Vector2[4]{
        _nativePolygonList[pointsIndexOffset + 0],
        _nativePolygonList[pointsIndexOffset + 1],
        _nativePolygonList[pointsIndexOffset + 2],
        _nativePolygonList[pointsIndexOffset + 3],
    });
               
    pointsIndexOffset += 4;
}

This seems kind of crazy, because doing this in 3D is 1000x faster, even before using the async api for Physics.BakeMesh in the job system.

Unity’s time spent on trying to sort all these shapes…

My code’s time spent generating this list…
7621637--947831--Unity_hRGsJ3qoR2.png

Surely there HAS to be a better way?!

You’re doing this on an enabled PolygonCollider2D so you have to expect it to update the path when you set it right? Each path is not independent, all the paths define the total physics shapes so you’re repeating work constantly.

Ensure that the PolygonCollider2D is disabled, make as many changes as you like then enable it. That way, it’ll look at the final result only.

Also, it’s not producing garbage, you are by creating arrays. Use the “List” overload and reuse the list.

Also, now in 2021.2 there’s a new CustomCollider2D that gives you raw access to produce PhysicsShape2D. This is by far the fastest way to write physics shapes. You can even tessellate your own, in a job/burst and just fire them to the collider. It means you can use unlimited physics shapes of different types in a single collider. This is supplemented by the new GetShapes methods which retrieve physics shapes from any collider/rigidbody (all colliders).

Fantastic. Thanks for the info!

7624768--948472--upload_2021-11-2_20-21-45.png

Just verifying, yes it is MUCH faster with the following code:

polygon2D.enabled = false;
polygon2D.pathCount = _nativePolygonList.Length / 4;

var cache = new Vector2[4];

var pointsIndexOffset = 0;
for (var i = 0; i < _nativePolygonList.Length / 4 - 1; ++i)
{
    cache[0] = _nativePolygonList[pointsIndexOffset + 0];
    cache[1] = _nativePolygonList[pointsIndexOffset + 1];
    cache[2] = _nativePolygonList[pointsIndexOffset + 2];
    cache[3] = _nativePolygonList[pointsIndexOffset + 3];

    polygon2D.SetPath(i, cache);

    pointsIndexOffset += 4;
}

polygon2D.enabled = true;

This is still pretty slow though! So I’ll try out your suggestion of CustomCollider2D next.

Alright, so using CustomCollider2D is the for sure winner. Nice!

The creation is now much faster, however I’m finding that the contact generation stage seems to be quite slow now. Hmm. I imagine I must be doing something wrong with the physics shape creation? I only need boxes, but I need quite a lot of them. They’re all convex, thankfully, so hopefully there’s a solution here.

7624849--948502--upload_2021-11-2_21-15-59.png

My shape generation code looks like this:

var physicsShape = new PhysicsShape2D()
{
    shapeType = PhysicsShapeType2D.Polygon,
    vertexStartIndex = pointIndex,
    vertexCount = 4,
};
           
PhysicsShapes.Add(physicsShape);

And I set it like this:

customCollider2D.enabled = false;

if(_nativeCustomShapes.Length > 0)
{
    customCollider2D.SetCustomShapes(_nativeCustomShapes, _nativePolygonList);
}
else
{
    customCollider2D.ClearCustomShapes();
}

customCollider2D.enabled = true;

@MelvMay any additional info would be greatly appreciated!

The underlying physics system doesn’t know anything about Unity colliders, it’s just physics shapes so it won’t have any effect on contact generation. For instance, a CircleCollider2D just produces a circle shape, a BoxCollider2D produces a single polygon shape, a PolygonCollider2D just produces multiple polygon shapes etc. Polygon shapes created with a PolygonCollider2D or a BoxCollider2D are identical, they are just polygon shapes.

Contacts are obviously only produced when there’s an overlap between shapes on different colliders so it’s as simple as that. If you have 10,000 shapes in a collider, it wouldn’t matter. It’d only matter if all those shapes overlapped another collider and you were asking for contacts. The profiler shows how many contacts are being produced too so maybe look at that as you’re likely to have a lot being produce. Impossible to really say more.

In terms of the code you posted, you don’t need to disable the CustomCollider2D because it is not doing what the PolygonCollider2D is doing. The PolygonCollider2D takes in outlines (paths) which the physics system knows nothing about. These have to be tessellated into convex polygons (real physics polygon shapes). That takes time. If you set multiple paths, it keeps doing this each call.

The CustomCollider2D doesn’t interpret paths because it takes real shapes and can create/update them as is. No intermediary work required. When you set the shapes, it’ll even check each shape to see if it already exists and not modify it but whatever it does, it does quickly. If you change the radius/vertex on a shape, it can change that single thing alone without it affecting anything else; this is the reason for it being quick alongside being able to accept a new list of shapes and being able to ignore ones that have no changed etc.

The way you should be using it is using a PhysicsShapeGroup2D. You create whatever you need in that, then just provide it that. The shape group has things like AddBox, AddPolygon etc. If you reuse the shape group, it’ll not allocate too as its capacity will automatically increase. Note: There’s a bug which has been fixed and is waiting to land that fixes a small GC allocation here.

Of course, as you show, there’s an overload to SetCustomShapes that allows you to provide even lower-level raw native array shapes/vertices but it requires more attention to detail as the code example shows.

If you have a simple reproduction case you could create in a project then feel free to send it me either DM me or if you like, DM me with your email address and I can set-up a private place for you to upload it.

Thanks for the detailed response, this is great. I managed to get contact generation down by just setting a minimum shape size in my handling of custom shapes, which seems to almost completely solve my issues (in my case, this is okay). Really happy with the results.

My only remaining question is: is there a way to do this off the main thread? Is customCollider2D.SetCustomShapes(native) thread-safe? And if it is not, can it be made thread safe in the future somehow?

As for contact generations, I’ll try to get a repro case and send it along, because I do think there was something fishy going on. I’m always getting contacts generated, despite not having any other physics 2d components in the scene during that profile.

Anything’s possible I guess however Box2D (the physics engine) isn’t thread safe at all so any calls to it would have to be completely blocked whilst it happened.

You’re saying a single collider is producing contacts? That’s not possible if this is what you’re saying. physics shapes attached to the same body cannot produce contacts, they’re not even considered due to how contact island processing works. You have to have another collider (physics shapes) on another body contacting them. Are you sure you don’t have some trigger covering it all etc?

Yeah this is reasonable, if I was using the job system to do this I would just call Complete() before physics are ran.

Sorry I made a mistake, there was one collider in the scene I had forgot to disable when I was testing this. My bad.

Thanks again for all the help!

I’d be interested in seeing any updates and/or performance differences here. This is a new feature so getting feedback is most welcome. Also just seeing it allowing you to do stuff you otherwise wouldn’t is good too.