-
Notifications
You must be signed in to change notification settings - Fork 0
Rendering
This page describes how the game world is drawn
The first important concept in rendering is the idea of a scanline. These are a line of map tiles which have the same on-screen Y co-ordinate. For example, consider the following scene:
Considering the same scene, the scanlines can be pulled apart:
Another way of thinking of scanlines is by considering the X and Y co-ordinates of each tile, at which point each scanline is a set of tiles for which the sum X+Y is constant.
Obviously, drawing the game world simply involves drawing all the tiles, walls, and entities which make up the world. The difficult part is drawing all these things in the correct order. Rendering is done in a 2D manner, and no depth sorting is performed, so things appear on-screen in the order in which they are rendered - if two things overlap, then the one which was rendered last will be the one appearing on-screen.
The first things to be rendered are the floor tiles. The order in which floor tiles are rendered is unspecified, as floor tiles do not overlap. The rendering engine is free to render the floor tiles in whatever order is most convenient or efficient for it.
Scanlines are important as they form the first level of ordering. For everything after the floor (i.e. shadows, walls, and entities), the world is drawn one scanline at a time, starting at the screen top, and working downward. Letting "scanline N" refer to the scanline whose tiles satisfy X+Y=N, everything on scanline N is rendered before everything on scanline N+1.
Before considering how a scanline is rendered, it is useful to recap on exactly what needs to be rendered.
A scanline is a set of tiles. If we ignore the floor, as that has already been rendered, then each tile has (or can have):
- A half or full shadow on the floor.
- A wall to the north, optionally with a shadow cast on it.
- A wall to the west (as far as the renderer is concerned, decorations like hedgerows and lampposts are just walls).
- A UI floor overlay (used when dragging out a room blueprint, for the footprint when placing an object, etc.)
- A list of zero or more entities.
That last item is important; for the purposes of rendering, each entity is associated with a single tile. For entities which span exactly one tile, the tile they are associated with is trivial, unless the entity can move (i.e. walk) between tiles. For entities which span multiple tiles, properties like render_attach_position
and use_animate_from_use_position
determine which tile is associated with the entity. As a brief aside, entities which can move are classed as Humanoids
, and all occupy a single tile, whilst entities which are static are classed as Objects
, and can span multiple tiles.
Up until now, a scanline has been a set of tiles, and sets have no defined order. This isn't particularly useful, as rendering is all about having an order. There are two natural ways to order a scanline:
- By increasing X co-ordinate (left to right on-screen)
- By increasing Y co-ordinate (right to left on-screen)
Slightly confusingly, both of these orders are used when rendering a scanline. This is because a scanline is rendered in three phases:
- For each tile from left to right, the floor shadow.
- For each tile from right to left, the north wall (optionally with shadow), then some of that tile's entities.
- For each tile from left to right, the west wall, then the UI floor overlay, then the remainder of that tile's entities.
Splitting up the rendering of entities might initially seem a bit odd, but there are good reasons for doing so. Consider the following two situations:
In these two situations, we have a humanoid walking between two tiles, with a line of walls on two sides. Let us de-construct the first of these situations:
Here we have one entity (the humanoid walking between tiles), two floor tiles, three scanlines, and four walls. The order of rendering should be:
- The floor tiles
- Scanline 0
- The west wall of the upper tile
- Scanline 1
- The west wall of the lower tile
- The humanoid
- The east wall of the upper tile (the west wall of the tile to the right of the lower tile)
- Scanline 2
- The east wall of the lower tile (the west wall of a tile which is not included in the image)
Now let us de-construct the second of these situations:
Again we have one entity, two floor tiles, three scanlines, and four walls. This time, the order of rendering should be:
- The floor tiles
- Scanline 0
- The north wall of the upper tile
- Scanline 1
- The north wall of the lower tile
- The humanoid
- The south wall of the upper tile (the north wall of the tile to the left of the lower tile)
- Scanline 2
- The south wall of the lower tile (the north wall of a tile which is not included in the image)
To get the correct rendering order for the first situation, we need to draw west walls from left to right, and entities from left to right after the west wall. To get the correct rendering order for the second situation, we need to draw north walls from right to left, and entities from right to left after the north wall. Hence for each tile, we need to split the list of entities into two parts, and render them at different points in the scanline rendering process.
In the Contents of a Scanline section, it was stated that each tile had a list of entities, and in the Rendering of a Scanline section, it was concluded that this list is infact two lists. These lists have names:
- The early list (drawn when iterating through tiles from right to left, between north walls).
- The normal list (drawn when iterating through tiles from left to right, after all the north walls, and between west walls).
By default, entities are placed on the normal list. To place an entity on the early list, make sure that the THDF_EarlyList
flag (value 1024) is set on the entity's animation when setTile
is called. To place an entity on the normal list, make sure that flag is not set when setTile
is called. Note that changing the flag has no effect until you set the tile, which is why code like humanoid.th:setTile(humanoid.th:getTile())
is occasionally seen after setting the animation flags. At a higher level, an object is placed on the early list when idle if the early_list
property is set to true
in the object's orientation, and is placed on the early list when in-use if the early_list_while_in_use
property is set to true
in the object's orientation.
For most entities, correct rendering behaviour can be achieved by carefully selecting the correct tile and list to render the entity from. However in some cases, the correct rendering behaviour cannot be achieved in this manner. In such cases, an alternative solution is available: splitting the rendering of an entity across multiple tiles. In this system, the entity is placed in the normal list for multiple tiles, and when it comes to rendering a tile, only those columns of pixels of the entity which intersect said tile are drawn. For example, in the following image, the rendering of the sofa is split over two tiles. The two tiles are marked with strong red and blue outlines. All of the sofa's pixels falling within the red rectangle get rendered in the normal list for the red tile, and all the sofa's pixels falling within the blue rectangle are rendered in the normal list for the blue tile.
Back to the Home page