Skip to content

Y-sort

In a top-down game something lower on the screen should draw in front of something higher up, so a character walking below a tree covers its trunk and one walking above it is hidden behind the leaves. nim2d draws in the order you issue the calls, so the way to get that is to draw everything in order of its Y, lowest first, so the nearest thing lands on top. Doing that by hand means keeping a list and sorting it every frame. The ysort module does that for you. It is an opt-in module, imported on its own with import nim2d/ysort.

The one type is a DrawQueue. You make one with newDrawQueue, keep it across frames, and each frame you queue the things to draw with the Y to sort them by, then flush draws them all in order and empties the queue for the next frame.

import nim2d/ysort

type Sprite = object
  x, y: float   # y is the feet, where the sprite meets the ground
  image: Image

let queue = newDrawQueue[Sprite]()

n2d.draw = proc(nim2d: Nim2d) =
  for s in sprites:
    queue.add(s.y, s)
  queue.flush(nim2d, proc(nim2d: Nim2d, s: Sprite) =
    s.image.draw(nim2d, s.x, s.y))

The queue is generic over your own sprite or entity type, so newDrawQueue[Sprite]() makes a queue of Sprite. add takes the Y to sort by and the item itself, and flush calls a draw proc once per item, in order, with the item you queued. The item is stored as plain data rather than as a closure over your loop, which avoids the Nim problem where several closures made inside one loop all end up capturing the loop's final value, and it means no closure is allocated per sprite.

The sort key is the part worth getting right. Use the sprite's feet, the base Y where it stands on the ground, not its center. If you sort tall sprites by their center a character can stand clearly in front of a tree and still be drawn behind it, because the tree's center sits lower than the character's. Sorting by the base lines the overlap up with where things actually touch the ground. A larger Y means lower on the screen, which draws later and so lands in front.

For things that should always stay above or below the sorted sprites, the layered add overload takes an integer layer. Lower layers draw first, underneath higher ones, and the Y-sort happens within each layer. A ground or floor layer at 0, the moving characters at 1, and a roof or a heads-up layer at 2 keep those bands in order while the characters still sort among themselves. Items at the same layer and Y keep the order they were added in, so two sprites level with each other stay put instead of swapping and flickering from frame to frame.

Beyond that there is len for how many items are queued and clear to drop them without drawing, for a frame you decide to skip.

The ysort example walks a player through a field of trees, all drawn by their feet, so the player passes behind the ones lower on the screen and in front of the ones higher up. Space toggles the sorting so you can see it break and work.

See also

The runnable ysort example, and the ysort API reference.