coffee icon indicating copy to clipboard operation
coffee copied to clipboard

Direct spritesheet support

Open BobG1983 opened this issue 4 years ago • 4 comments

I currently have the following really basic function:

fn rect_from_tilemap_coord(x: u16, y: u16, tile_size: Option<u16>) -> Rectangle<u16>{
  let tile_size = tile_size.unwrap_or(16);
  let x = max(0, x - 1);
  let y = max(0, y - 1);
  return Rectangle {
    x: tile_size * x + x,
    y: tile_size * y + y,
    width: tile_size,
    height: tile_size
  }
}

Which is basically a really hacky way to get a Rectangle that represents an area of a spritesheet. I'm likely to create an enum of all the sprites in the spritesheet, but I was wondering if there was some "better" way to do this in the actual engine. A SpriteSheet that wraps an Image and takes grid coords rather than a quad maybe? Or some way to load a spritesheet from which you can get the source rectangle for a given sprite?

It may be that the best way to do it is indeed for me to create myself a tilemap class, that the renderer owns and have an enum that the renderer knows how to convert into a rectangle, but I thought I'd bring this up.

BobG1983 avatar Sep 08 '19 21:09 BobG1983

A SpriteSheet type is definitely something to consider adding to the engine.

It's not there yet because there are many different approaches we could take and I had a hard time choosing one. Thus, I preferred to leave it up to users to implement one and wait until we see a pattern or a common approach, and design based on that.

Ideally, I would like something as type-safe as possible (i.e. something that avoids referencing sprites out of bounds, like the texture_array module). However, I am not sure if the amount of type-safety we can get is worth it in this case.

For instance, enums work well for item sprites, but once you add animation into the mix they become a hassle.

Overall, I am unsure if there is a generic approach to satisfy most use cases here without promoting bad practice. We may be missing additional concepts, maybe we need different types of sprite sheets... I will keep thinking about it!

If anyone has built something similar or has any ideas, feel free to share your thoughts here. It may help us find a good solution.


@RantingBob Meanwhile, for your use case, if you need to refer individually to each sprite, I would use an enum and have a struct wrapping an Image or a Batch taking only values of this enum to perform draw operations.

hecrj avatar Sep 09 '19 18:09 hecrj

I don't have any ideas at this time. I'll implement something and come back to you. As an aside, Batch vs Image for spritesheets?

BobG1983 avatar Sep 09 '19 19:09 BobG1983

@RantingBob Batch will allow you to draw many sprites at once efficiently. If you are drawing a tilemap, it's definitely the recommended approach.

hecrj avatar Sep 10 '19 16:09 hecrj

I just started messing around with Coffee, but this is what I made to handle sprite sheets. Functionality is very basic, but seems to work ok. Still needs some sanity checking like making sure a requested sprite is actually within bounds or that a given image can fit the stated number of rows and columns.

use coffee::graphics::{Point, Rectangle, Image, Sprite};

//size of a single sprite in the Sprite Sheet
struct SpriteSize { pub width: u16, pub height: u16}

//An array of sprites packed into a single image, also called an Atlas.
pub struct SpriteSheet {
    atlas: Image,
    sprite_size: SpriteSize,
}


impl SpriteSheet {

    pub fn new(image: Image, rows: u16, columns: u16) -> SpriteSheet {
        let sprite_size = SpriteSize {
            width: image.width() / columns,
            height: image.height() / rows,
        };

        SpriteSheet {
            atlas: image,
            sprite_size,
        }
    }


    //Extract a specific sprite from the sprite sheet
    // @position: provided position of the sprite on the target screen/frame/etc.
    // @row: Which row of the atlas we are requesting. Note row starts at 1
    // @column: Which column of the atlas we are requesting. Note column starts at 1
    // returns: Image object of full atlas, used in drawing 
    //          Sprite object depicting a single sprite in the atlas

    pub fn get_sprite(&self, position: Point, mut row: u16, mut column: u16)
           -> (Image, Sprite) {

        //adjust row/column for calculating sprite position in atlas  
        row = row - 1;
        column = column - 1;

        //define these for brevity
        let sprite_width = self.sprite_size.width;
        let sprite_height = self.sprite_size.height;

        //return full atlas image and requested sprite's location in the atlas.
        (self.atlas.clone(), //note coffee docs says cloning Image is very cheap
         Sprite { 
            source: Rectangle{
                x: column * sprite_width, y: row * sprite_height, 
                width: sprite_width, height: sprite_height,
            },
            position: position, 
            scale: (1.0,1.0) //assume normal scale, lets other code change it as needed 
        })
    }
}

Usage in Game::draw is like this:

let position = Point::new(100.0, 100.0);
let (image, sprite) = self.player_spritesheet.get_sprite(position, 5, 1);
image.draw(sprite, &mut frame.as_target());

Feel free to use it. Might be an ok starting point.

DomtronVox avatar Aug 03 '20 03:08 DomtronVox