Class: Ruby2D::Sprite
Overview
An animated sprite from a single image, a horizontal strip sprite sheet, or a SpriteSheet (texture atlas).
Constant Summary
Constants included from Interactive
Interactive::OBJECT_EVENTS, Interactive::OBJECT_EVENT_FILTER_PREDICATES
Instance Attribute Summary collapse
-
#clip_height ⇒ Object
Returns the value of attribute clip_height.
-
#clip_width ⇒ Object
Returns the value of attribute clip_width.
-
#clip_x ⇒ Object
Returns the value of attribute clip_x.
-
#clip_y ⇒ Object
Returns the value of attribute clip_y.
-
#flip ⇒ Object
Returns the value of attribute flip.
-
#frame ⇒ Object
Returns the value of attribute frame.
-
#sheet ⇒ Object
readonly
Returns the value of attribute sheet.
-
#speed ⇒ Object
Returns the value of attribute speed.
Attributes inherited from Image
#height, #path, #rotate, #width
Attributes included from Renderable
#color, #height, #padding_bottom, #padding_left, #padding_right, #padding_top, #visible, #width, #x, #x_align, #y, #y_align, #z
Instance Method Summary collapse
-
#_render_scene ⇒ Object
Scene-graph draw hook (see Renderable#_render_scene): advance the animation and draw the current frame, minus
render's keyword handling — a zero-arg call into the 11-keywordrenderstill pays ~5µs of keyword setup on wasm mruby, half a millisecond per frame at 100 sprites. -
#height=(h) ⇒ Object
Set the displayed height.
-
#initialize(source, width: nil, height: nil, x: 0, y: 0, z: 0, rotate: 0, rx: nil, ry: nil, tint: nil, opacity: nil, loop: false, time: 300, speed: 1.0, animations: nil, default: 0, frame: nil, clip_x: 0, clip_y: 0, clip_width: nil, clip_height: nil, add: true, visible: true, padding: nil, padding_top: nil, padding_right: nil, padding_bottom: nil, padding_left: nil) ⇒ Sprite
constructor
Create a sprite.
-
#loop=(value) ⇒ Object
Set whether the current animation loops, mid-play, without restarting it (unlike re-calling
play). -
#looping? ⇒ Boolean
Whether the current animation loops.
-
#pause ⇒ Object
Pause the current animation on its current frame.
-
#paused? ⇒ Boolean
Whether the sprite is currently paused.
-
#play(animation: :default, loop: nil, flip: nil, &done_proc) ⇒ Object
Start playing an animation.
-
#playing? ⇒ Boolean
Whether an animation is actively playing.
-
#render(x: nil, y: nil, width: nil, height: nil, rotate: nil, clip_x: nil, clip_y: nil, clip_width: nil, clip_height: nil, tint: nil, opacity: nil) ⇒ Object
Render the sprite.
-
#resize!(width = @width, height = @height) ⇒ Object
A SpriteSheet-backed sprite shares one backing texture with every other sprite cut from the same sheet.
-
#resume ⇒ Object
Resume the animation paused by
pause, picking up at the current frame. -
#stop(animation = nil) ⇒ Object
Stop the current animation and set to the default frame.
-
#update(dt = UPDATE_DT_UNSET) ⇒ Object
Advance the animation by one frame of real time and update the clip rect.
-
#width=(w) ⇒ Object
Set the displayed width.
Methods inherited from Image
#rx, #rx=, #ry, #ry=, #tint, #tint=, #x=, #y=
Methods included from Renderable
#_alignment_anchor_dx, #_alignment_anchor_dy, #_apply_padding, #_extract_alignment, #_point_in_polygon?, #_point_on_segment?, #_require_numeric_position, #_resolve_alignment, #_unrotate, #_validate_dimensions, #add, #colour=, #contains?, flatten_color, flatten_per_vertex, flatten_points, flatten_resolved_color, #hide, #opacity, #opacity=, #padding=, #remove, resolve_color_or_default, resolve_single_color, #show
Methods included from Interactive
#_fire_event, #interactive?, #off, #on
Constructor Details
#initialize(source, width: nil, height: nil, x: 0, y: 0, z: 0, rotate: 0, rx: nil, ry: nil, tint: nil, opacity: nil, loop: false, time: 300, speed: 1.0, animations: nil, default: 0, frame: nil, clip_x: 0, clip_y: 0, clip_width: nil, clip_height: nil, add: true, visible: true, padding: nil, padding_top: nil, padding_right: nil, padding_bottom: nil, padding_left: nil) ⇒ Sprite
Create a sprite. source can be either an image path or a SpriteSheet
instance. When given a SpriteSheet, animations may reference frames by
name (strings or { name:, time: } hashes) and frame: selects a
single static frame.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/ruby2d/sprite.rb', line 21 def initialize(source, width: nil, height: nil, x: 0, y: 0, z: 0, rotate: 0, rx: nil, ry: nil, tint: nil, opacity: nil, loop: false, time: 300, speed: 1.0, animations: nil, default: 0, frame: nil, clip_x: 0, clip_y: 0, clip_width: nil, clip_height: nil, add: true, visible: true, padding: nil, padding_top: nil, padding_right: nil, padding_bottom: nil, padding_left: nil) @sheet = source.is_a?(SpriteSheet) ? source : nil if @sheet super(nil, x: x, y: y, z: z, rotate: rotate, rx: rx, ry: ry, tint: tint, opacity: opacity, add: false, padding: padding, padding_top: padding_top, padding_right: padding_right, padding_bottom: padding_bottom, padding_left: padding_left, _share_from: @sheet.texture) else super(source, x: x, y: y, z: z, rotate: rotate, rx: rx, ry: ry, tint: tint, opacity: opacity, add: false, padding: padding, padding_top: padding_top, padding_right: padding_right, padding_bottom: padding_bottom, padding_left: padding_left) end # Stash the resolved sheet rect (if any) so trim metadata can be # applied after @clip_width/@clip_height get their final values. sheet_rect = nil if frame raise Error, '`frame:` requires a SpriteSheet source' unless @sheet sheet_rect = lookup_sheet_frame(frame.to_s) clip_x = sheet_rect[:x] clip_y = sheet_rect[:y] clip_width = sheet_rect[:width] clip_height = sheet_rect[:height] @frame = frame.to_s end @img_width = @orig_width @img_height = @orig_height @flip = nil @loop = loop @frame_time = time self.speed = speed @animations = normalize_animations(animations || {}) @current_frame = default # When a sheet is in use and no explicit clip / frame was given, fall back # to the first frame in the sheet so the sprite shows something useful. if @sheet && !frame && clip_width.nil? && clip_height.nil? first = first_sheet_rect if first sheet_rect = first clip_x = first[:x] clip_y = first[:y] clip_width = first[:width] clip_height = first[:height] end end @clip_x = clip_x @clip_y = clip_y @clip_width = (clip_width || @img_width ).to_i @clip_height = (clip_height || @img_height).to_i @clip_width = @img_width if @clip_width <= 0 @clip_height = @img_height if @clip_height <= 0 # Trim metadata. For atlas frames it can come from the sheet rect; # for path-based sprites and untrimmed atlas frames it falls back # to the no-trim defaults (source = clip, trim = 0). With those # defaults, the C draw path's trim math collapses to today's # untrimmed behavior. @source_width = (sheet_rect && sheet_rect[:source_width]) || @clip_width @source_height = (sheet_rect && sheet_rect[:source_height]) || @clip_height @trim_x = (sheet_rect && sheet_rect[:trim_x]) || 0 @trim_y = (sheet_rect && sheet_rect[:trim_y]) || 0 # Track user-provided display dimensions so animations can update # @width/@height to match the source (footprint) dimensions when no # explicit size was given. @user_width = width @user_height = height @width = width || @source_width @height = height || @source_height @clipped = true setup_animation @visible = visible self.add if add end |
Instance Attribute Details
#clip_height ⇒ Object
Returns the value of attribute clip_height.
8 9 10 |
# File 'lib/ruby2d/sprite.rb', line 8 def clip_height @clip_height end |
#clip_width ⇒ Object
Returns the value of attribute clip_width.
8 9 10 |
# File 'lib/ruby2d/sprite.rb', line 8 def clip_width @clip_width end |
#clip_x ⇒ Object
Returns the value of attribute clip_x.
8 9 10 |
# File 'lib/ruby2d/sprite.rb', line 8 def clip_x @clip_x end |
#clip_y ⇒ Object
Returns the value of attribute clip_y.
8 9 10 |
# File 'lib/ruby2d/sprite.rb', line 8 def clip_y @clip_y end |
#flip ⇒ Object
Returns the value of attribute flip.
7 8 9 |
# File 'lib/ruby2d/sprite.rb', line 7 def flip @flip end |
#frame ⇒ Object
Returns the value of attribute frame.
7 8 9 |
# File 'lib/ruby2d/sprite.rb', line 7 def frame @frame end |
#sheet ⇒ Object (readonly)
Returns the value of attribute sheet.
7 8 9 |
# File 'lib/ruby2d/sprite.rb', line 7 def sheet @sheet end |
#speed ⇒ Object
Returns the value of attribute speed.
7 8 9 |
# File 'lib/ruby2d/sprite.rb', line 7 def speed @speed end |
Instance Method Details
#_render_scene ⇒ Object
Scene-graph draw hook (see Renderable#_render_scene): advance the
animation and draw the current frame, minus render's keyword handling
— a zero-arg call into the 11-keyword render still pays ~5µs of
keyword setup on wasm mruby, half a millisecond per frame at 100 sprites.
421 422 423 424 425 |
# File 'lib/ruby2d/sprite.rb', line 421 def _render_scene _resolve_alignment update Ext.image_draw(self) end |
#height=(h) ⇒ Object
Set the displayed height. See width= — persists across frames; nil resumes
source-tracking.
145 146 147 148 |
# File 'lib/ruby2d/sprite.rb', line 145 def height=(h) @user_height = h @height = h end |
#loop=(value) ⇒ Object
Set whether the current animation loops, mid-play, without restarting it
(unlike re-calling play). Takes effect at the next loop boundary while
the animation is still playing: turning it off lets a cycling animation
finish — and fire any completion block — when it next reaches the last
frame; turning it on keeps it cycling. It does not resume an animation
that has already finished and stopped — call play to restart that.
161 162 163 |
# File 'lib/ruby2d/sprite.rb', line 161 def loop=(value) @loop = value ? true : false end |
#looping? ⇒ Boolean
Whether the current animation loops
151 152 153 |
# File 'lib/ruby2d/sprite.rb', line 151 def looping? @loop end |
#pause ⇒ Object
Pause the current animation on its current frame. Idempotent and only meaningful while an animation is playing — calling it on an idle sprite is a no-op.
205 206 207 208 209 210 211 |
# File 'lib/ruby2d/sprite.rb', line 205 def pause return self unless @playing @playing = false @paused = true self end |
#paused? ⇒ Boolean
Whether the sprite is currently paused
226 227 228 |
# File 'lib/ruby2d/sprite.rb', line 226 def paused? @paused == true end |
#play(animation: :default, loop: nil, flip: nil, &done_proc) ⇒ Object
Start playing an animation. Pass loop: to override the sprite's default
loop setting and a block to run when a non-looping animation finishes.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/ruby2d/sprite.rb', line 167 def play(animation: :default, loop: nil, flip: nil, &done_proc) anim_name = animation || :default if @playing && anim_name == @playing_animation && flip == @flip # Same animation already running (same flip): don't restart it — that # would jump back to frame 0 — but still honor an explicitly-passed # `loop:` or completion block so callers can adjust them mid-play. # Anything left unset is preserved, so a per-frame `play(:state)` call # stays a safe no-op. @loop = loop ? true : false unless loop.nil? @done_proc = done_proc if done_proc else frames = @animations[anim_name] # Validate up front, before mutating any state, so a typo'd or empty # animation fails clearly here instead of crashing later in `update`. raise Error, "Animation `#{anim_name}` is not defined for this sprite" if frames.nil? raise Error, "Animation `#{anim_name}` has no frames" if frames.is_a?(Array) && frames.empty? @playing = true @paused = false @playing_animation = anim_name @done_proc = done_proc self.flip = flip loop = @defaults[:loop] if loop.nil? @loop = loop ? true : false set_frame @frame_budget = 0.0 # first frame gets its full duration end self end |
#playing? ⇒ Boolean
Whether an animation is actively playing. False when idle, paused, or held on the last frame of a finished non-looping animation.
232 233 234 |
# File 'lib/ruby2d/sprite.rb', line 232 def @playing == true end |
#render(x: nil, y: nil, width: nil, height: nil, rotate: nil, clip_x: nil, clip_y: nil, clip_width: nil, clip_height: nil, tint: nil, opacity: nil) ⇒ Object
Render the sprite. With no arguments it draws the same frame the scene
graph does (delegating to _render_scene) — advancing the animation and
drawing the current frame. Called with overrides for one-shot rendering
inside a render block (one-shot does not advance the animation).
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 |
# File 'lib/ruby2d/sprite.rb', line 358 def render(x: nil, y: nil, width: nil, height: nil, rotate: nil, clip_x: nil, clip_y: nil, clip_width: nil, clip_height: nil, tint: nil, opacity: nil) if x.nil? && y.nil? && width.nil? && height.nil? && rotate.nil? && clip_x.nil? && clip_y.nil? && clip_width.nil? && clip_height.nil? && tint.nil? && opacity.nil? return _render_scene end Window.render_ready_check saved_x, saved_y = @x, @y saved_width, saved_height = @width, @height saved_rotate = @rotate saved_clip_x, saved_clip_y = @clip_x, @clip_y saved_clip_width, saved_clip_height = @clip_width, @clip_height saved_source_w, saved_source_h = @source_width, @source_height saved_trim_x, saved_trim_y = @trim_x, @trim_y saved_color = @color @x = x if x @y = y if y @width = width if width @height = height if height @rotate = rotate if rotate @clip_x = clip_x if clip_x @clip_y = clip_y if clip_y @clip_width = clip_width if clip_width @clip_height = clip_height if clip_height # Override draws use no trim — the caller is being explicit about # source rect and display size, so collapse the trim math to the # straightforward `draw clip into (x, y, width, height)` case. @source_width = @clip_width @source_height = @clip_height @trim_x = 0 @trim_y = 0 if tint || opacity @color = tint ? Color.new(tint) : Color.new(saved_color) @color.opacity = opacity if opacity end begin Ext.image_draw(self) ensure @x, @y = saved_x, saved_y @width, @height = saved_width, saved_height @rotate = saved_rotate @clip_x, @clip_y = saved_clip_x, saved_clip_y @clip_width, @clip_height = saved_clip_width, saved_clip_height @source_width, @source_height = saved_source_w, saved_source_h @trim_x, @trim_y = saved_trim_x, saved_trim_y @color = saved_color end end |
#resize!(width = @width, height = @height) ⇒ Object
A SpriteSheet-backed sprite shares one backing texture with every other sprite cut from the same sheet. Re-rasterizing it (Image#resize!) would silently corrupt all of them and invalidate the sheet's frame coordinates, so refuse it. Path/strip-backed sprites own their texture and resize fine.
122 123 124 125 126 127 128 129 130 131 |
# File 'lib/ruby2d/sprite.rb', line 122 def resize!(width = @width, height = @height) if @sheet raise Error, 'Cannot resize! a SpriteSheet-backed sprite: its texture is shared ' \ 'by every sprite cut from the same sheet. Use a standalone Image, or ' \ 'set width/height to change only this sprite\'s display size.' end super end |
#resume ⇒ Object
Resume the animation paused by pause, picking up at the current
frame. The frame budget is reset so the first frame after resume
gets its full duration.
216 217 218 219 220 221 222 223 |
# File 'lib/ruby2d/sprite.rb', line 216 def resume return self unless @paused @paused = false @playing = true @frame_budget = 0.0 self end |
#stop(animation = nil) ⇒ Object
Stop the current animation and set to the default frame
271 272 273 274 275 276 277 278 279 |
# File 'lib/ruby2d/sprite.rb', line 271 def stop(animation = nil) return unless !animation || animation == @playing_animation @playing = false @paused = false @playing_animation = @defaults[:animation] @current_frame = @defaults[:frame] set_frame end |
#update(dt = UPDATE_DT_UNSET) ⇒ Object
Advance the animation by one frame of real time and update the clip rect.
Called with no arguments from the scene-graph loop, where dt defaults to
the engine's shared frame delta (Window.delta_time) — the same value an
update do |dt| block receives. Driving every sprite off that one clock
(rather than each polling its own) keeps them in lockstep, lets the engine
clamp stalls once, and makes update(dt) directly testable. Pass an
explicit dt (in seconds) to drive the animation by hand.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/ruby2d/sprite.rb', line 297 def update(dt = UPDATE_DT_UNSET) return unless @playing # Resolve the shared frame delta only now that we know the sprite is # playing — the no-arg scene-graph call hits this every frame per sprite. dt = Window.delta_time if dt.equal?(UPDATE_DT_UNSET) # Bank the elapsed time, scaled by `@speed` (0.0 freezes, 2.0 is double # speed), then spend it one whole frame at a time. Looping here — rather # than a single step per call — lets a high `speed` skip frames and a long # frame catch up, instead of capping at one frame per tick. Each frame is # charged its own `time:`, and the unspent remainder stays banked so # playback doesn't slowly drift. The budget is in milliseconds. @frame_budget += dt * @speed * 1000.0 cycle = @last_frame - @first_frame + 1 # frames in one full loop steps = 0 finished = false while @playing && steps < cycle ft = @frame_time || @defaults[:frame_time] break if ft.nil? || ft <= 0 || @frame_budget < ft @frame_budget -= ft steps += 1 @current_frame += 1 if @current_frame > @last_frame if @loop @current_frame = @first_frame else # Hold on the last frame and stop advancing. The user can call # `stop` (or `play` something else) to leave the pose. This lets a # death animation linger on its corpse pose, a jump animation hold # mid-air, an attack hold its follow-through, etc. @current_frame = @last_frame @playing = false finished = true end end set_frame # refresh the clip rect and pick up the next frame's `time:` end # Cap a runaway-fast loop (an absurd `speed`) at one cycle per update so it # can't spin; drop the unspent budget rather than letting it grow unbounded. @frame_budget = 0.0 if @playing && steps == cycle && @frame_budget >= (@frame_time || @defaults[:frame_time]) # Fire the completion block last — after the bookkeeping above — so a # `play` chained inside it has the final say. Clear it first so the chained # play can install its own block without us seeing a stale reference. return unless finished && @done_proc kept_done_proc = @done_proc @done_proc = nil kept_done_proc.call end |
#width=(w) ⇒ Object
Set the displayed width. Like the width: constructor option, this persists
across animation frames (stored as the user override). A sprite without an
explicit size tracks each frame's source dimensions; set to nil to resume
that source-tracking. Overrides Image's plain attr_accessor, which wrote
@width directly and was silently reset by the next animation tick.
138 139 140 141 |
# File 'lib/ruby2d/sprite.rb', line 138 def width=(w) @user_width = w @width = w end |