I'd second Zibelas's recommendation in the comments to use a particle system.
You can create an array to hold the information about all of your shapes:
const int MAX_SHAPES = 5000; ParticleSystem.Particle[] _shapeBuffer = new ParticleSystem.Particle[MAX_SHAPES];
Then iterate over that array to update the positions / rotations / shapes / colours / etc. of the shapes you want to draw.
for (int i = 0; i < currentShapeCount; i++) { var particle = _shapeBuffer[i]; particle.position = UpdateShapePosition(particle, i); particle.startColor = UpdateShapeColor(particle, i); particle.startSize = UpdateShapeSize(particle, i); particle.rotation = UpdateShapeAngle(particle, i); // Keep this particle alive forever. particle.startLifetime = float.MaxValue; particle.remainingLifetime = float.MaxValue; _shapeBuffer[i] = particle; } for (i = currentShapeCount; i < MAX_SHAPES; i++) { var particle = _shapeBuffer[i]; // This kills the particle. ;) particle.remainingLifetime = -1; _shapeBuffer[i] = particle; }
Then write your changed buffer back to the particle system to be drawn:
particleSystem.SetParticles(_shapeBuffer, MAX_SHAPES, 0);
If your shapes should move in predictable ways and only a few of them need custom updates, you can delegate that to the particle system by assigning particles a velocity / angular velocity, and using GetParticles to read its updates back into your C#-side buffer when you want to intervene.
You can even use the flipbook feature to draw both your circles and squares with a single buffer, without any shader magic, by using the randomSeed or remainingLifetime parameters to control which flipbook page this particle uses.
The job system is also great for big batch work like this, but it's a bigger commitment with a steeper learning curve and initial development cost than a quick-and-dirty abuse of the particle renderer. 😉