I am working on a 2D shmup that requires rendering thousands of animated bullets and animated medals on screen at the same time.

I use a standard pooling method to activate and deactivate these objects when they are needed, rather than make instantiation calls during gameplay.

When I first set up my objects using SpriteRenderer and Animator, I found that Unity was unable to render everything performantly, and the culprit according to the profiler was tons of calls to animation system. Using native SpriteRenderer and Animator just doesn't seem to work when you're trying to render thousands of sprites at once, even if you're using a pooling system and have no physics on the objects whatsoever.

Because of this, I switched to using a custom system to draw my bullets and medals. The system works as follows.

Each bullet (and medal, for that matter) is a bare bones object w/ nothing more than a position, a vel, and a reference to an instance of a custom class called SpriteSheetAnimationData. This class stores frameCount, currentFrame, frameTimer, frameExtent, Vector4 uv, and Matrix4x4 matrix. When an instance of the bullet is needed, it is activated from my bullet pool and its animation data is reset.

I also have a custom object called RenderingManager which contains references to the following.

Material bulletMaterial
Mesh bulletMesh
List<Matrix4x4> bulletMatrixList
List<Vector4> bulletUVList
List<SpriteSheetAnimationData> bulletAnimationData

Each frame, I iterate through active bullets in my pool, update their positions based on their vel, do simple collision check using squared distance, and update the bullet's SpriteSheetAnimationData instance based on the bullet's position and how much time has elapsed since the previous frame.

Then, in my RenderingManager, I use the single bulletMaterial and bulletQuad to draw all the bullets with a single call of DrawMeshInstanced(). It looks something like the following (albeit for medals, not bullets):

    // MEDALS

    // NOTE: DrawMeshInstanced() can only draw 1023 meshes in a single call, so we need to batch
    int sliceCount = 1023;
    for (int i = 0; i < medal_animation_data_list.Count; i += sliceCount)
        int sliceSize = Mathf.Min(medal_animation_data_list.Count - i, sliceCount);

        // create list of matrices and uvs for every entity, in order to pass lists to DrawMeshInstanced()
        for (int j = 0; j < sliceSize; j++)
            MedalSpriteSheetAnimationData animation_data = medal_animation_data_list[i + j];
            int frameCount = animation_data.frameCount;
            int currentFrame = animation_data.currentFrame;
            float uvWidth = 1f / animation_data.frameCount;
            float uvHeight = 1f;
            float uvOffsetX = uvWidth * animation_data.currentFrame;
            float uvOffsetY = 0f;
            Vector4 uv = new Vector4(uvWidth, uvHeight, uvOffsetX, uvOffsetY);
            animation_data.uv = uv;
            Matrix4x4 matrix = animation_data.matrix;

        materialPropertyBlock.SetVectorArray(shaderPropertyId, medalUVList);

            UnityEngine.Rendering.ShadowCastingMode.Off, // shadow casting mode
            false, // receive shadows
            0, // layer
            camera // camera

        // clear matrices before next iteration of for loop

This works like an absolute charm! It allows me to draw thousands of animated bullets and medals on screen at the same time with no slowdown.

Further, I am able to always draw the bullets on top of the medals by setting my bulletMaterial render queue to 3001, and the medalMaterial render queue to 3000.

enter image description here enter image description here

The problem I now face is that I have some normal GameObjects which I need to render in between my bullets and medals. For example, Pickup which uses SpriteRenderer and Animator, and SpecialText which uses TextMeshPro.

Because DrawMeshInstanced() is only able to draw into the Default Sorting Layer, and I have these other objects on custom Sorting Layers, they always appear behind the medals and bullets, rather than in between them like I need.

I am more than willing to simply set all my stuff to the Default Layer if it could help, but I still wouldn't know how to get them to appear between the bullets and the medals, since the latter use the special rendering system.

Any advice on how to solve this issue and render my normal objects between the two objects I have using the custom rendering system, which is pinned to Default Layer?

  • 1
    \$\begingroup\$ In the past I've had luck doing something similar by abusing a particle system. These are also optimized for drawing tons of quads or small meshes efficiently, and they have options to work with sorting layers. You can use the particle system's API to overwrite the particle data with your own, positioning/moving/animating the instances in a completely custom way. \$\endgroup\$
    – DMGregory
    Oct 21 at 21:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.