Class: RGame::Core::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/core/renderer.rb,
ext/rgame_core/ruby/renderer_ext.c

Overview

What a game draws with.

class MyGame < RGame::Core::App
def initialize
  super(width: 800, height: 600, caption: 'demo')
  @renderer = RGame::Core::Renderer.new(self)
  @hero = RGame::Core::Image.new(self, 'hero.png')
end

def draw
  @renderer.rect(10, 10, 100, 40, color: RGame::Util::Color::WHITE)
  @renderer.image(@hero, 400, 300, angle: 45)
end
end

Drawing is only legal inside draw, and calling one of these outside it raises. That is on purpose: the frame is not open, so the vertices would be silently discarded, and an invisible failure is the worst kind.

Nothing is drawn immediately. Calls accumulate and are sorted when the frame closes, so what ends up on top is decided by z rather than by call order. Equal z keeps call order, which is what stops same-layer sprites flickering between frames.

z: is an offset inside the current layer, not a global number

A z: is added to whatever #layered most recently pushed, and must be within RGame::Util::Z::Z_MIN..Z_MAX — one node's worth of room. It orders that node's own drawing (its panel under its label, a shadow under its sprite) and can reach nothing else. Which node comes before which is the scene graph's business, resolved by the traversal and handed here as a layer; see RGame::Util::Z.

A caller outside any #layered block draws at the base layer 0, which is what a spec or a bare script gets.

The C half of this class (ext/rgame_core/ruby/renderer_ext.c) has the draw_* and push_* primitives; everything here is the comfortable surface over them.

Colours

Every drawing method takes color:, accepting whatever Color.coerce does: nil (white — an untinted draw), [r, g, b], [r, g, b, a], or a RGame::Util::Color. Passing a Color is the allocation-free path and is what per-frame code should do; an array allocates one colour per call.

Constant Summary collapse

Color =
RGame::Util::Color
Z =
RGame::Util::Z
SHAPE_Z =

Shapes default above sprites, so a debug box or a health bar drawn without a z: lands on top of that node's sprite rather than under it. Well inside one slot, so the defaults order a node's own drawing and nothing further.

50
IMAGE_Z =
0
CIRCLE_SEGMENTS =

Enough segments that a circle reads as round at the sizes a 2D game draws one, and few enough that a screenful of them is still one batch.

64
TEXT_Z =

Text defaults above sprites but below shapes, within the same slot.

10
FONT_SIZE =
18
DEBUG_BOX_COLOR =

Translucent red, for #debug_box.

Color.new(255, 40, 40, 120)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'ext/rgame_core/ruby/renderer_ext.c', line 98

static VALUE renderer_initialize(VALUE self, VALUE app) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);

    ref->app = rgame_app_unwrap(app); /* raises TypeError on anything else */
    ref->app_object = app;
    ref->recorded_images = Qnil;
    return self;
}

Instance Attribute Details

#assetsObject

Returns the value of attribute assets.



92
93
94
# File 'lib/rgame/core/renderer.rb', line 92

def assets
  @assets
end

#fontObject

The font this renderer draws with when a call does not name one.

Built on first use rather than in the constructor: creating a font needs a GL context, and a renderer is often built before there is one. Set your own with #font= to change what every unqualified #text call uses.



309
310
311
# File 'lib/rgame/core/renderer.rb', line 309

def font
  @font ||= Font.new(app, FONT_SIZE)
end

Class Method Details

.new(app, assets: nil) ⇒ Object

assets: is where a draw id that is not registered gets resolved from, and defaults to the app's own manager — so the common case wires itself and renderer.sprite('hero.json', …) works with nothing set up.

A Ruby self.new because the C initialize has fixed arity and no business knowing what an asset manager is; the same shape Font's path: uses.



86
87
88
89
90
# File 'lib/rgame/core/renderer.rb', line 86

def self.new(app, assets: nil)
  renderer = super(app)
  renderer.assets = assets.nil? ? app.assets : assets
  renderer
end

Instance Method Details

#appObject



115
116
117
118
119
# File 'ext/rgame_core/ruby/renderer_ext.c', line 115

static VALUE renderer_app(VALUE self) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);
    return ref->app_object;
}

#background(image, x = 0, y = 0, z: IMAGE_Z, color: nil) ⇒ Object

An image with its top-left at (x, y), at its natural size — a full-screen backdrop by default. image_at with both scales at 1, kept because "put this at the origin" is worth a name of its own.



212
213
214
# File 'lib/rgame/core/renderer.rb', line 212

def background(image, x = 0, y = 0, z: IMAGE_Z, color: nil)
  draw_image(resolve_image(image), x, y, Z.offset(z), packed(color))
end

#begin_recordObject



329
330
331
332
333
334
335
336
337
338
# File 'ext/rgame_core/ruby/renderer_ext.c', line 329

static VALUE renderer_begin_record(VALUE self) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);

    if (!rgame_app_begin_record(drawing_app(self))) {
        rb_raise(rb_eRuntimeError, "already recording (recordings do not nest)");
    }
    ref->recorded_images = rb_ary_new();
    return self;
}

#cancel_recordObject



355
356
357
358
359
360
361
362
# File 'ext/rgame_core/ruby/renderer_ext.c', line 355

static VALUE renderer_cancel_record(VALUE self) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);

    ref->recorded_images = Qnil;
    rgame_app_cancel_record(ref->app);
    return self;
}

#circle(cx, cy, radius, z: SHAPE_Z, color: nil, segments: CIRCLE_SEGMENTS) ⇒ Object

A filled circle, as a fan of triangles around its centre.



181
182
183
# File 'lib/rgame/core/renderer.rb', line 181

def circle(cx, cy, radius, z: SHAPE_Z, color: nil, segments: CIRCLE_SEGMENTS)
  draw_circle(cx, cy, radius, segments, Z.offset(z), packed(color))
end

#clipped(x, y, width, height) ⇒ Object

Everything drawn in the block is confined to the given rectangle.

A clip only ever narrows: nesting one inside another intersects them, so a child cannot draw outside the region its parent allowed. Give each player a clipped block and you have split-screen.



295
296
297
298
299
300
301
302
# File 'lib/rgame/core/renderer.rb', line 295

def clipped(x, y, width, height)
  push_clip(x, y, width, height)
  begin
    yield
  ensure
    pop
  end
end

#debug_box(x, y, width, height, z: SHAPE_Z) ⇒ Object

A translucent overlay for visualising a collision box, so a scene can ask for one without knowing what colour "debug" is.



360
361
362
# File 'lib/rgame/core/renderer.rb', line 360

def debug_box(x, y, width, height, z: SHAPE_Z)
  rect(x, y, width, height, z: z, color: DEBUG_BOX_COLOR)
end

#draw_circle(*args) ⇒ Object



175
176
177
178
179
180
181
182
# File 'ext/rgame_core/ruby/renderer_ext.c', line 175

static VALUE renderer_draw_circle(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 6, 6);

    rgame_app_draw_circle(drawing_app(self), (float)NUM2DBL(argv[0]), (float)NUM2DBL(argv[1]),
                          (float)NUM2DBL(argv[2]), NUM2INT(argv[3]), packed_color(argv[5]),
                          NUM2DBL(argv[4]));
    return self;
}

#draw_image(image, x, y, z, color) ⇒ Object



206
207
208
209
210
211
212
213
214
# File 'ext/rgame_core/ruby/renderer_ext.c', line 206

static VALUE renderer_draw_image(VALUE self, VALUE image, VALUE x, VALUE y, VALUE z,
                                 VALUE color) {
    check_drawn(rgame_app_draw_image(drawing_app(self), rgame_image_unwrap(image),
                                     (float)NUM2DBL(x), (float)NUM2DBL(y), packed_color(color),
                                     NUM2DBL(z)),
                image);
    note_recorded_image(self, image);
    return self;
}

#draw_image_rot(*args) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
# File 'ext/rgame_core/ruby/renderer_ext.c', line 230

static VALUE renderer_draw_image_rot(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 7, 7);

    check_drawn(rgame_app_draw_image_rot(drawing_app(self), rgame_image_unwrap(argv[0]),
                                         (float)NUM2DBL(argv[1]), (float)NUM2DBL(argv[2]),
                                         (float)NUM2DBL(argv[3]), (float)NUM2DBL(argv[4]),
                                         packed_color(argv[6]), NUM2DBL(argv[5])),
                argv[0]);
    note_recorded_image(self, argv[0]);
    return self;
}

#draw_image_scaled(*args) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
# File 'ext/rgame_core/ruby/renderer_ext.c', line 218

static VALUE renderer_draw_image_scaled(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 7, 7);

    check_drawn(rgame_app_draw_image_scaled(drawing_app(self), rgame_image_unwrap(argv[0]),
                                            (float)NUM2DBL(argv[1]), (float)NUM2DBL(argv[2]),
                                            (float)NUM2DBL(argv[3]), (float)NUM2DBL(argv[4]),
                                            packed_color(argv[6]), NUM2DBL(argv[5])),
                argv[0]);
    note_recorded_image(self, argv[0]);
    return self;
}

#draw_line(*args) ⇒ Object



166
167
168
169
170
171
172
173
# File 'ext/rgame_core/ruby/renderer_ext.c', line 166

static VALUE renderer_draw_line(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 7, 7);

    rgame_app_draw_line(drawing_app(self), (float)NUM2DBL(argv[0]), (float)NUM2DBL(argv[1]),
                        (float)NUM2DBL(argv[2]), (float)NUM2DBL(argv[3]),
                        (float)NUM2DBL(argv[4]), packed_color(argv[6]), NUM2DBL(argv[5]));
    return self;
}

#draw_quad(*args) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
# File 'ext/rgame_core/ruby/renderer_ext.c', line 142

static VALUE renderer_draw_quad(int argc, VALUE *argv, VALUE self) {
    /* Ten positional arguments is past the point where naming each parameter
     * helps, so this one takes an argv and unpacks it in a loop. */
    rb_check_arity(argc, 10, 10);

    float xy8[8];
    for (int i = 0; i < 8; i++) {
        xy8[i] = (float)NUM2DBL(argv[i]);
    }
    rgame_app_draw_quad(drawing_app(self), xy8, packed_color(argv[9]), NUM2DBL(argv[8]));
    return self;
}

#draw_rect(x, y, width, height, z, color) ⇒ Object



134
135
136
137
138
139
140
# File 'ext/rgame_core/ruby/renderer_ext.c', line 134

static VALUE renderer_draw_rect(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height,
                                VALUE z, VALUE color) {
    rgame_app_draw_rect(drawing_app(self), (float)NUM2DBL(x), (float)NUM2DBL(y),
                        (float)NUM2DBL(width), (float)NUM2DBL(height), packed_color(color),
                        NUM2DBL(z));
    return self;
}

#draw_text(*args) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'ext/rgame_core/ruby/renderer_ext.c', line 248

static VALUE renderer_draw_text(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 6, 6);

    VALUE font = argv[0];
    VALUE string = argv[1];
    /* RSTRING_PTR on a non-String reads whatever the object's second word
     * happens to be and hands it to C as a char* — a segfault, not an
     * exception. Everything else here goes through NUM2DBL or an unwrap, which
     * type-check on the way; this is the one argument that has to say so.
     * StringValue converts what can convert and raises TypeError otherwise. */
    StringValue(string);
    const char *text = RSTRING_PTR(string);
    long length = RSTRING_LEN(string);

    int drawn = rgame_app_draw_text(drawing_app(self), rgame_font_unwrap(font), text,
                                    (size_t)length, (float)NUM2DBL(argv[2]),
                                    (float)NUM2DBL(argv[3]), packed_color(argv[5]),
                                    NUM2DBL(argv[4]));
    /* RSTRING_PTR hands out a pointer the collector does not know about. */
    RB_GC_GUARD(string);

    check_drawn(drawn, font);
    return self;
}

#draw_triangle(*args) ⇒ Object



155
156
157
158
159
160
161
162
163
164
# File 'ext/rgame_core/ruby/renderer_ext.c', line 155

static VALUE renderer_draw_triangle(int argc, VALUE *argv, VALUE self) {
    rb_check_arity(argc, 8, 8);

    float xy6[6];
    for (int i = 0; i < 6; i++) {
        xy6[i] = (float)NUM2DBL(argv[i]);
    }
    rgame_app_draw_triangle(drawing_app(self), xy6, packed_color(argv[7]), NUM2DBL(argv[6]));
    return self;
}

#drawing?Object



122
123
124
125
126
# File 'ext/rgame_core/ruby/renderer_ext.c', line 122

static VALUE renderer_drawing_p(VALUE self) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);
    return ref->app && rgame_app_is_drawing(ref->app) ? Qtrue : Qfalse;
}

#end_recordObject



340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'ext/rgame_core/ruby/renderer_ext.c', line 340

static VALUE renderer_end_record(VALUE self) {
    rgame_renderer_ref *ref;
    TypedData_Get_Struct(self, rgame_renderer_ref, &renderer_data_type, ref);

    VALUE images = ref->recorded_images;
    ref->recorded_images = Qnil;

    rgame_recording *recording = rgame_app_end_record(ref->app);
    if (!recording) {
        rb_raise(rb_eRuntimeError, "no recording is open");
    }

    return rgame_recording_wrap(recording, ref->app_object, ref->app, images);
}

#image(image, cx, cy, angle: 0, scale: 1, z: IMAGE_Z, color: nil) ⇒ Object

An image centred on (cx, cy), rotated angle degrees clockwise about that centre and uniformly scaled. Unrotated and unscaled is a fast path that skips the transform stack entirely.

Takes an Image or an id for one — see #resolve_image.



190
191
192
# File 'lib/rgame/core/renderer.rb', line 190

def image(image, cx, cy, angle: 0, scale: 1, z: IMAGE_Z, color: nil)
  draw_image_rot(resolve_image(image), cx, cy, angle, scale, Z.offset(z), packed(color))
end

#image_at(image, x, y, scale_x: 1, scale_y: 1, z: IMAGE_Z, color: nil) ⇒ Object

An image with its top-left at (x, y), scaled independently per axis — a tile, a nine-slice corner, a sprite-sheet frame.

A negative scale mirrors the image inside the same rectangle; it does not move it. So a frame drawn at (x, y) covers the same pixels whichever way it faces, and scale_x: -1 means "facing the other way" rather than "one width to the left":

renderer.image_at(frame, x, y, scale_x: facing_left ? -1 : 1)

A zero scale draws nothing.



205
206
207
# File 'lib/rgame/core/renderer.rb', line 205

def image_at(image, x, y, scale_x: 1, scale_y: 1, z: IMAGE_Z, color: nil)
  draw_image_scaled(resolve_image(image), x, y, scale_x, scale_y, Z.offset(z), packed(color))
end

#layerObject



311
312
313
# File 'ext/rgame_core/ruby/renderer_ext.c', line 311

static VALUE renderer_layer(VALUE self) {
    return DBL2NUM(rgame_app_layer(drawing_app(self)));
}

#layered(band = Z::DEFAULT) ⇒ Object

Everything drawn in the block draws in its own layer: a fresh slot in band, which every z: inside is then an offset from.

renderer.layered(:hud) { renderer.text(score, 12, 10) }

Two things fall out of it, and they are the whole of draw order. Slots are handed out in the order they are asked for, so nesting this the way a scene graph is nested makes draw order tree order — a node drawn later is in front, and its whole subtree with it. And a band is a hard partition: every slot in :hud is above every slot in :world, whatever either drew, because they are 2**40 apart and a z: cannot reach out of one slot. See RGame::Util::Z.

A caller that never uses this draws at layer 0 and gets exactly the z it passes, which is what a spec or a one-off script wants.



277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/rgame/core/renderer.rb', line 277

def layered(band = Z::DEFAULT)
  index = Z.index(band)
  push_layer(Z.slot_base(index, next_layer_slot(index)))
  begin
    yield
  ensure
    # An ensure for the same reason `rotated` has one: a scene that raises
    # mid-draw would otherwise leave every later node drawing in its
    # layer, and the frame would come out interleaved with no clue why.
    pop
  end
end

#line(x1, y1, x2, y2, thickness: 1.0, z: SHAPE_Z, color: nil) ⇒ Object

A line of real thickness — drawn as a quad, because GL's own line width is a suggestion drivers may ignore above one pixel.



176
177
178
# File 'lib/rgame/core/renderer.rb', line 176

def line(x1, y1, x2, y2, thickness: 1.0, z: SHAPE_Z, color: nil)
  draw_line(x1, y1, x2, y2, thickness, Z.offset(z), packed(color))
end

#next_layer_slot(band) ⇒ Object



316
317
318
# File 'ext/rgame_core/ruby/renderer_ext.c', line 316

static VALUE renderer_next_layer_slot(VALUE self, VALUE band) {
    return UINT2NUM(rgame_app_next_layer_slot(drawing_app(self), NUM2INT(band)));
}

#nine_slice(id, x, y, width, height, z: IMAGE_Z, tint: nil) ⇒ Object

A nine-slice filling (x, y, width, height), tinted by tint if given. Registration only: a nine-slice id names an element of an atlas, not a file, so there is nothing for the asset manager to resolve it to.



131
132
133
# File 'lib/rgame/core/renderer.rb', line 131

def nine_slice(id, x, y, width, height, z: IMAGE_Z, tint: nil)
  lookup(:nine_slice, id).draw(self, x, y, width, height, z: z, color: tint)
end

#popObject



364
365
366
367
# File 'ext/rgame_core/ruby/renderer_ext.c', line 364

static VALUE renderer_pop(VALUE self) {
    rgame_app_pop(drawing_app(self));
    return self;
}

#push_clip(x, y, width, height) ⇒ Object



289
290
291
292
293
294
295
296
297
298
# File 'ext/rgame_core/ruby/renderer_ext.c', line 289

static VALUE renderer_push_clip(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height) {
    if (!rgame_app_push_clip(drawing_app(self), NUM2INT(x), NUM2INT(y), NUM2INT(width),
                             NUM2INT(height))) {
        /* Clipping cannot be baked into a recording; see core.h. Saying so
         * beats recording geometry that quietly ignores the clip. */
        rb_raise(rb_eRuntimeError,
                 "a clip cannot be recorded — wrap the replay in #clipped instead");
    }
    return self;
}

#push_layer(base) ⇒ Object



306
307
308
309
# File 'ext/rgame_core/ruby/renderer_ext.c', line 306

static VALUE renderer_push_layer(VALUE self, VALUE base) {
    rgame_app_push_layer(drawing_app(self), NUM2DBL(base));
    return self;
}

#push_rotate(degrees, pivot_x, pivot_y) ⇒ Object



278
279
280
281
282
# File 'ext/rgame_core/ruby/renderer_ext.c', line 278

static VALUE renderer_push_rotate(VALUE self, VALUE degrees, VALUE pivot_x, VALUE pivot_y) {
    rgame_app_push_rotate(drawing_app(self), (float)NUM2DBL(degrees), (float)NUM2DBL(pivot_x),
                          (float)NUM2DBL(pivot_y));
    return self;
}

#push_scale(sx, sy) ⇒ Object



284
285
286
287
# File 'ext/rgame_core/ruby/renderer_ext.c', line 284

static VALUE renderer_push_scale(VALUE self, VALUE sx, VALUE sy) {
    rgame_app_push_scale(drawing_app(self), (float)NUM2DBL(sx), (float)NUM2DBL(sy));
    return self;
}

#push_translate(dx, dy) ⇒ Object



273
274
275
276
# File 'ext/rgame_core/ruby/renderer_ext.c', line 273

static VALUE renderer_push_translate(VALUE self, VALUE dx, VALUE dy) {
    rgame_app_push_translate(drawing_app(self), (float)NUM2DBL(dx), (float)NUM2DBL(dy));
    return self;
}

#quad(x1, y1, x2, y2, x3, y3, x4, y4, z: SHAPE_Z, color: nil) ⇒ Object

Four arbitrary points, in loop order: listing them in Z order gives an hourglass rather than a shape.



166
167
168
# File 'lib/rgame/core/renderer.rb', line 166

def quad(x1, y1, x2, y2, x3, y3, x4, y4, z: SHAPE_Z, color: nil)
  draw_quad(x1, y1, x2, y2, x3, y3, x4, y4, Z.offset(z), packed(color))
end

#recordObject

Bakes everything the block draws into a RGame::Core::Recording, which can then be replayed for the cost of one call per texture however many draws went into it. Nothing is drawn now — the block's output goes into the recording instead of into this frame.

ground = renderer.record { tiles.each { |t| renderer.image(t.img, t.x, t.y) } }
ground.draw(-camera.x, -camera.y)

Recording happens inside draw like everything else, and does not nest. A clip pushed inside the block raises: clipping cannot be baked, so clip the replay instead. See RGame::Core::Recording.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/rgame/core/renderer.rb', line 343

def record
  begin_record
  completed = false
  begin
    yield
    completed = true
  ensure
    # A block that raised leaves a half-built recording open, and the
    # next frame would keep drawing into it. Unwinding here means the
    # exception is the only thing the caller has to deal with.
    cancel_record unless completed
  end
  end_record
end

#rect(x, y, width, height, z: SHAPE_Z, color: nil) ⇒ Object

A filled axis-aligned rectangle.



160
161
162
# File 'lib/rgame/core/renderer.rb', line 160

def rect(x, y, width, height, z: SHAPE_Z, color: nil)
  draw_rect(x, y, width, height, Z.offset(z), packed(color))
end

#register_image(id, image) ⇒ Object

--- draw-by-id ---------------------------------------------------------

Game logic names assets, it does not hold them: the engine layer may hold RGame::Util values but no RGame::Core handle at all, so a Symbol or a path is the only thing a node can carry. Resolving it is this side of the boundary's job.

An id is normally a root-relative path, resolved through the asset manager and then remembered, so a per-frame draw neither re-resolves nor allocates a lookup key:

renderer.sprite('example 09/player.json', row, col, x, y)

register_* pre-binds an id to a chosen object, for the two things a path cannot name: an id that is not a file (nine-slice ids are atlas element names) and an object the game assembled itself.



111
# File 'lib/rgame/core/renderer.rb', line 111

def register_image(id, image) = registry(:image)[id] = image

#register_nine_slice(id, nine_slice) ⇒ Object



114
# File 'lib/rgame/core/renderer.rb', line 114

def register_nine_slice(id, nine_slice) = registry(:nine_slice)[id] = nine_slice

#register_sheet(id, sheet) ⇒ Object



112
# File 'lib/rgame/core/renderer.rb', line 112

def register_sheet(id, sheet) = registry(:sheet)[id] = sheet

#register_tilemap(id, tilemap) ⇒ Object



113
# File 'lib/rgame/core/renderer.rb', line 113

def register_tilemap(id, tilemap) = registry(:tilemap)[id] = tilemap

#register_ui_atlas(atlas) ⇒ Object

Registers every element of a UiAtlas under its own name, since those names are what a widget asks for.



118
119
120
121
# File 'lib/rgame/core/renderer.rb', line 118

def register_ui_atlas(atlas)
  atlas.nine_slices.each { |id, nine_slice| register_nine_slice(id, nine_slice) }
  self
end

#rotated(angle, pivot_x, pivot_y) ⇒ Object

Everything drawn in the block is rotated angle degrees about (pivot_x, pivot_y), so a node can spin all of its parts coherently around one point.

A zero angle skips the push entirely — unrotated drawing pays nothing, which matters because most drawing is unrotated.



222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/rgame/core/renderer.rb', line 222

def rotated(angle, pivot_x, pivot_y)
  return yield if angle.zero?

  push_rotate(angle, pivot_x, pivot_y)
  begin
    yield
  ensure
    # An ensure, not a plain pop: a scene that raises mid-draw would
    # otherwise leave the stack deeper than it found it, and every
    # later frame would draw askew for a reason nothing points at.
    pop
  end
end

#scaled(sx, sy = sx) ⇒ Object



251
252
253
254
255
256
257
258
259
260
# File 'lib/rgame/core/renderer.rb', line 251

def scaled(sx, sy = sx)
  return yield if sx == 1 && sy == 1

  push_scale(sx, sy)
  begin
    yield
  ensure
    pop
  end
end

#sprite(id, row, col, x, y, flip_x: false, z: IMAGE_Z) ⇒ Object

One frame of a registered or resolvable sprite sheet, top-left at (x, y).



124
125
126
# File 'lib/rgame/core/renderer.rb', line 124

def sprite(id, row, col, x, y, flip_x: false, z: IMAGE_Z)
  lookup(:sheet, id).draw(self, row, col, x, y, flip_x: flip_x, z: z)
end

#text(string, x, y, z: TEXT_Z, color: nil, font: nil) ⇒ Object

One line of text, with its top-left corner at (x, y) — the same corner every other drawing method takes, rather than the baseline typography would use.

Newlines are not special. A caller wanting two lines draws two, stepping by #text_height.



321
322
323
# File 'lib/rgame/core/renderer.rb', line 321

def text(string, x, y, z: TEXT_Z, color: nil, font: nil)
  draw_text(font || self.font, string, x, y, Z.offset(z), packed(color))
end

#text_height(font: nil) ⇒ Object

The line height: what to step y by for a second line.



330
# File 'lib/rgame/core/renderer.rb', line 330

def text_height(font: nil) = (font || self.font).height

#text_width(string, font: nil) ⇒ Object

What #text would occupy, for centring and layout. Unlike the drawing methods this works outside draw, because measuring touches no GL.



327
# File 'lib/rgame/core/renderer.rb', line 327

def text_width(string, font: nil) = (font || self.font).text_width(string)

#tilemap(id, layer, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0) ⇒ Object

One layer of a tile map — the layer a Tiled .tmx lists at layer, counting from the bottom.

One layer rather than the whole map, because a scene draws its actors between two of them: trunks under, canopies over. Which is which is the scene tree's business — RGame::Engine::TileMapLayer mounts a node per layer — so this takes no z:.

Drawn in world coordinates: a tile at column 3 lands at 3 * tile_width, and getting it onto the screen is the caller's transform, like every other drawing method here. The rectangle is a cull rect — which part of the world is worth drawing — so a camera supplies it but does not move the result. That is what lets one map be drawn through several cameras in a frame.

elapsed is the seconds its animated tiles have been running for, and is an argument rather than a clock read on purpose — see CLAUDE.md, "draw renders state; time enters through update". A scene accumulates it in update, which is what makes pausing work.



154
155
156
157
# File 'lib/rgame/core/renderer.rb', line 154

def tilemap(id, layer, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0)
  lookup(:tilemap, id)
    .draw_layer(self, layer, cull_x, cull_y, cull_width, cull_height, elapsed: elapsed)
end

#translated(dx, dy) ⇒ Object

Everything drawn in the block is shifted by (dx, dy) screen pixels — the camera's view transform. Because it is a draw-time transform rather than something baked into positions, the same world can be drawn again under a different offset and clip, which is what split-screen is.



240
241
242
243
244
245
246
247
248
249
# File 'lib/rgame/core/renderer.rb', line 240

def translated(dx, dy)
  return yield if dx.zero? && dy.zero?

  push_translate(dx, dy)
  begin
    yield
  ensure
    pop
  end
end

#triangle(x1, y1, x2, y2, x3, y3, z: SHAPE_Z, color: nil) ⇒ Object



170
171
172
# File 'lib/rgame/core/renderer.rb', line 170

def triangle(x1, y1, x2, y2, x3, y3, z: SHAPE_Z, color: nil)
  draw_triangle(x1, y1, x2, y2, x3, y3, Z.offset(z), packed(color))
end