Class: RGame::Core::SpriteSheet

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/core/sprite_sheet.rb

Overview

A texture atlas — one image plus a JSON descriptor — sliced into frames and drawn one at a time.

sheet = RGame::Core::SpriteSheet.load(app, 'media/hero.json')
sheet.draw(renderer, row, col, x, y, flip_x: facing_left, z: 10)

The descriptor

{
"image": "hero.png",
"frame_width": 16, "frame_height": 24,
"cell_width": 32, "cell_height": 32,
"origin_x": 8, "origin_y": 4,
"animations": { "walk_left": { "row": 1, "frames": 4, "fps": 8 } }
}

image is resolved next to the descriptor. The rest describes the grid.

A frame can be smaller than its cell. Cells are laid out on a fixed cell_width x cell_height grid, but what is drawn is a frame_width x frame_height rectangle offset by origin_x/origin_y inside the cell. That is what lets a sheet whose cells are sized for the widest pose — an attack, a swing — still expose a tight, centred box for walking, so a character does not appear to change size between animations. Leave the cell and origin keys out and frame == cell, which is what a simple sheet wants.

animations is handed back untouched by #animations. This class knows nothing about time; the scene layer builds its own animation table from that hash, which is why the raw form is what gets exposed.

Slicing costs nothing

Every frame is cut once, at construction, as a view onto the one upload — so a sheet of two hundred frames is two hundred small objects and a single texture, and #draw is an array index plus one call.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(image, atlas) ⇒ SpriteSheet

atlas is the parsed descriptor; image an already-loaded sheet image.



61
62
63
64
65
66
67
# File 'lib/rgame/core/sprite_sheet.rb', line 61

def initialize(image, atlas)
  @frame_width = atlas.fetch(:frame_width) { missing(:frame_width) }
  @frame_height = atlas.fetch(:frame_height) { missing(:frame_height) }
  @animations = atlas[:animations] || {}

  @frames = slice(image, atlas)
end

Instance Attribute Details

#animationsObject (readonly)

Returns the value of attribute animations.



46
47
48
# File 'lib/rgame/core/sprite_sheet.rb', line 46

def animations
  @animations
end

#frame_heightObject (readonly)

Returns the value of attribute frame_height.



46
47
48
# File 'lib/rgame/core/sprite_sheet.rb', line 46

def frame_height
  @frame_height
end

#frame_widthObject (readonly)

Returns the value of attribute frame_width.



46
47
48
# File 'lib/rgame/core/sprite_sheet.rb', line 46

def frame_width
  @frame_width
end

Class Method Details

.load(app, atlas_path) ⇒ Object

Loads a descriptor and the image beside it.

The AssetManager does not use this — it calls .new with an image it has already cached, so a sheet's PNG is shared with a standalone load of the same file. This is the standalone path, for a game with one sheet and no asset manager.



54
55
56
57
58
# File 'lib/rgame/core/sprite_sheet.rb', line 54

def self.load(app, atlas_path)
  atlas = JSON.parse(File.read(atlas_path), symbolize_names: true)
  directory = File.dirname(File.expand_path(atlas_path))
  new(Image.new(app, File.join(directory, atlas[:image])), atlas)
end

Instance Method Details

#draw(renderer, row, col, x, y, flip_x: false, z: 0) ⇒ Object

Draws one frame with its top-left at (x, y).

flip_x mirrors the frame within that same rectangle, so a character occupies the same pixels whichever way it faces — see RGame::Core::Renderer#image_at.



74
75
76
# File 'lib/rgame/core/sprite_sheet.rb', line 74

def draw(renderer, row, col, x, y, flip_x: false, z: 0)
  renderer.image_at(@frames[row][col], x, y, scale_x: flip_x ? -1 : 1, z: z)
end

#gridObject

How many frames the sheet was cut into, as [rows, columns].



79
# File 'lib/rgame/core/sprite_sheet.rb', line 79

def grid = [@frames.length, @frames.empty? ? 0 : @frames[0].length]