It's fun to run character simulations and generate results, it's even more fun when you can actually see the characters. These past two weeks I've been writing the sprite rendering system, that doesn't utilize gameobjects/monobehaviours, or unity spritesheets/texture atlases etc, it's all homemade. So, how is it done?

Instancing

For sprite rendering, there's no alternative, plain and simple. We have a very simple geometry (a quad), and we want to render N instances of it, each with its own parameters. The main parameters, for sprites, are:

  • Rect location in texture atlas
  • Number of animation frames (stored sequentially in atlas)
  • Current location in the world
  • Previous location in the world (moving sprites only)
  • Time at previous location in the world
    (moving sprites only)
  • Movement speed (moving sprites only)

From the above one can see that for moving sprites we need more data, actually it turns out to be twice as much. We can also reason that the list with the moving sprites needs to be updated far more often, due to the changing positions. Therefore, it makes sense to have two separated entity sprite lists, the static and the dynamic, each of which contains instance data that will be rendered with Unity's DrawMeshInstancedIndirect function.

Entity sprite list maintenance

  • Entity enters map: add its default/idle animation to the static list
  • Entity exits map: remove animation from static and dynamic lists
  • Entity teleports: remove animation from dynamic list,
    add it' default/idle animation to the static list
  • Entity moves to adjacent location: remove animation from static list, add its moving animation to the dynamic list
  • Every several frames, we go through the dynamic list and check if an animation has finished. If it has, we remove it from the list and add the entity's default/idle animation to the static list

For the above, when we add an animation, we also set the instance data to a CPU buffer. When we render, the CPU buffer contents are copied to a GPU ComputeBuffer (if anything has changed), which is sampled in the shader

Here's an example where every second, we schedule a movement to 10,000 entities. Runs pretty well except the last bullet point above, which I've disabled for this video. The video also shows an additional GUI sprite rendering layer with highlighted tiles (yellow squares) and the hovered tile is shown via a greenish sprite.