Class: RGame::Core::TileMapRenderer
- Inherits:
-
Object
- Object
- RGame::Core::TileMapRenderer
- Defined in:
- lib/rgame/core/tile_map_renderer.rb
Overview
Draws a tile map: the static layers baked once, the animated tiles drawn per frame, both culled to a rectangle of the world.
tiles = RGame::Core::TileMapRenderer.new(map, tileset_images)
map.layer_count.times do |layer|
tiles.draw_layer(renderer, layer, cull_x, cull_y, cull_w, cull_h,
elapsed: seconds)
end
It draws in world coordinates
A tile at column 3 is drawn at 3 * tile_width, and getting it onto the
screen is the caller's transform — the same deal every other drawable
gets. The rectangle passed in is therefore a cull rect and nothing
else: which part of the world is worth drawing.
It used to be both, offsetting the output by -camera as well as culling
to it, which worked exactly as long as there was one camera. Under
split-screen the same map is drawn through several, so a call that bakes
placement into its output can only be right for one of them. Culling is
genuinely per-camera; placement is the transform stack's job.
One call per layer, because the actors go between them
A layer is drawn on its own, and the order they are drawn in is the caller's. That is what lets a scene put its actors between two of them — tree trunks below, canopies above — which is the whole reason this does not simply draw the map in one go.
It is also why nothing here consults the map's above_layer? flag any
more: which layers cover the actors is a question about where the actors
are in the scene, and Tiled already answers "in what order do the layers
go" by listing them. RGame::Engine::TileMapLayer mounts one node per
layer and the tree does the rest.
What is baked and what is not
Within each layer, every tile that is not animated is baked into one recording, the first time that layer is drawn. Scrolling a baked layer is then one call per texture however many thousand tiles went into it. The handful that are animated are drawn individually each frame, culled to the viewport — a map far larger than the screen costs only what is on it.
Splitting per layer rather than into two bands bakes the same tiles into more recordings, not more vertices: the partition changed, the contents did not.
It loads nothing and holds no clock
The tiles arrive already sliced, so two maps sharing a tileset share one GPU upload — which is only true if something above pulled the image through the asset manager, and is why this class does not load its own.
And elapsed is an argument rather than a clock read, so animation is
something the caller advances. Pausing is "stop accumulating"; a spec
picks the frame it wants. See CLAUDE.md, "draw renders state; time
enters through update".
What it needs of a map
It never names the map's class — the tile map lives a layer above this
one and Core may not reach up (CLAUDE.md, "The rule points both ways").
What it calls is the 'a tile map' contract in
spec/support/shared_examples/: layer_count, width, height,
tile_width, tile_height, gid, and a tileset answering local_id,
animations and frame_local_id.
Instance Attribute Summary collapse
-
#map ⇒ Object
readonly
The map this was built from.
Instance Method Summary collapse
-
#draw_layer(renderer, index, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0) ⇒ Object
One layer, culled to
(cull_x, cull_y, cull_width, cull_height)in world coordinates and drawn in them. -
#initialize(map, tiles) ⇒ TileMapRenderer
constructor
tilesis the tileset image sliced into an Array indexed by local tile id — whatImage#tilesreturns. - #layer_count ⇒ Object
Constructor Details
#initialize(map, tiles) ⇒ TileMapRenderer
tiles is the tileset image sliced into an Array indexed by local tile
id — what Image#tiles returns.
79 80 81 82 83 84 85 86 87 |
# File 'lib/rgame/core/tile_map_renderer.rb', line 79 def initialize(map, tiles) @map = map @tileset = map.tileset @tiles = tiles @animated = collect_animated_tiles # Baked on first draw, not here: recording needs a live frame, and there # is no renderer at construction. @static = Array.new(map.layer_count) end |
Instance Attribute Details
#map ⇒ Object (readonly)
The map this was built from. A scene reads it for collision and world bounds, which are its business rather than this class's.
75 76 77 |
# File 'lib/rgame/core/tile_map_renderer.rb', line 75 def map @map end |
Instance Method Details
#draw_layer(renderer, index, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0) ⇒ Object
One layer, culled to (cull_x, cull_y, cull_width, cull_height) in world
coordinates and drawn in them.
The recording is replayed at its own origin, so it lands wherever the caller's transform puts it. That also makes it view-independent: one bake serves every viewport, which is what keeps split-screen affordable and is why the bake is not keyed on a camera. Baking happens on the first draw, and it is safe to do that inside a transform or a clip — recording runs on its own canvas, begun at identity, and captures neither.
No z:. A layer is drawn by a node of its own, so where it sits is the
scene tree's answer; everything this issues belongs to that one node and
goes in its slot.
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/rgame/core/tile_map_renderer.rb', line 105 def draw_layer(renderer, index, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0) unless index.is_a?(Integer) && index >= 0 && index < @static.size raise ArgumentError, "no layer #{index.inspect} in this map (it has #{@static.size})" end @static[index] ||= bake(renderer, index) @static[index].draw draw_animated(renderer, @animated[index], cull_x, cull_y, cull_width, cull_height, elapsed) end |
#layer_count ⇒ Object
89 |
# File 'lib/rgame/core/tile_map_renderer.rb', line 89 def layer_count = @map.layer_count |