Jump to content

  • Log In with Google      Sign In   
  • Create Account

SDL_tileset


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
No replies to this topic

#1 Servant of the Lord   Crossbones+   -  Reputation: 19588

Like
0Likes
Like

Posted 05 May 2009 - 08:49 AM

I spent a few hours today and sunday, tossing together a set of functions for SDL, allowing for easy use of tilesheets. To use it, there are two files you can include in your project: one header, and one source. The code is written clearly, and heavily commented, so if you want to see how it works, or want to change it, feel free.

It includes functions for loading (SDL_LoadTileset()) and freeing(SDL_FreeTileset()) tilesets, supports spacing between the tiles, and borders around the tileset, has a function for easily drawing a tile from the tileset (SDL_DrawTile()), and a function for setting the colorkey of the entire tileset (SDL_SetColorkey()).

You can also set the colorkey (or alpha!) of a tile individually, by getting the individual tile from the tileset using the provided function (SDL_GetTile).

There is also a special function to load a spritesheet (SDL_LoadSpritesheet()), where the individual sprites in the sheet are non-uniform sizes, and not neatly in a row. The function will automaticly detect where the sprites begin and end, without you having to specify the dimensions or locations of each sprite.


SDL_LoadTileset supports tilesets regardless of how they are stored, as long as they are the same size. For example, these are some tilesets I've tested it with:

Horizontally:
In a row

Vertically:
In a column

Both:
Vertically and horizontally

As mentioned, you can specify gaps between the tiles and spacing along the edges of the tileset, as long as you know beforehand the size of the spacing and borders:

gaps and borders

If you don't know the size of the spacing or borders, or if the spacing isn't uniform, you can use SDL_LoadSpritesheet() instead, and the function will automaticly recognize the individual images and seperate them (It's slower though).

Weird spacing, different sized images


Here's a screenshot of the example code:

Testing the functions


Since the code is only two source files, I'll just embed it here.

SDL_tilesets.h:
/*
	Created by: Servant of the Lord
	Created on: Sunday, May 3rd, 2009
	
	This is an example of loading a tileset in SDL, and keeping it in a form easy to manage.
	
	The code in these files are available for anyone to use, however they see fit, so long as
	they follow SDL's license. Redistribute it to your heart's desire.
	
	No credit is needed to be given; anyone can do whatever they want with this code. I retain
	no control or rights over it, do what you want with it. Use it and learn from it, if you
	desire to. If it benefits you, copy and paste it all you like.
	
	Warning: This is not officially part of the SDL library!
			 This code requires SDL and SDL_Image to run.
			 SDL can be found at http://www.libsdl.org/
*/


#include <iostream>
#include <vector>
#include <string>

#include "SDL\SDL.h"
#include "SDL\SDL_image.h"

#ifndef SDL_TILESET_H
#define SDL_TILESET_H

//Not a good idea (infact, it's a horrible idea), but available if someone wants them. =)
/*
	#define SDL_SpriteSheet SDL_Tileset
	#define SDL_FreeSpriteSheet SDL_FreeTileset
	#define SDL_DrawSprite SDL_DrawTile
	#define SDL_GetSprite SDL_GetTile
*/

//For ease of use, the vector that holds our images is being renamed to 'SDL_Tileset'
//Note that this means you can easily figure out the size of your tileset, by calling SDL_Tileset->size().
//Also, the first tile in the tileset, is the full tileset image, so your first tile starts at 1, not 0.
typedef std::vector<SDL_Surface*> SDL_Tileset;

//The first parameter is the name of the large tileset to load.
//The second and third parameters, are the sizes of each small tile. (For example, 32 pixels by 32 pixels)
//The third and fourth parameters allow the user to specify the size of a gap between each tile.
//The fourth and fifth parameters allow the user to the size of a border around the tileset.
SDL_Tileset *SDL_LoadTileset(std::string filename, int tileWidth, int tileHeight, int horizontalSpacing = 0, int verticalSpacing = 0, int horizontalBorder = 0, int verticalBorder = 0);

//The first parameter is the name of the spritesheet to load.
//The second parameter is the name of the spritesheet's background color.
//(Note: The background color must not be the same color as your colorkey. You should not use this color anywhere in your sprites)
//SDL_LoadSpritesheet does the same thing as SDL_LoadTileset, but tries to automaticly locate the sprites, by the background color around them.
//There must be at least 1 pixel of background color between every sprite.
//This function is slow! Luckily, you only need to call it once (per sheet). Still, it'd be
//better to just keep your sprites the same size, and load them with SDL_LoadTileset, when possible.
SDL_Tileset *SDL_LoadSpritesheet(std::string filename, SDL_Color backgroundColor);

//Frees the SDL_Tileset passed to it. The pointer will be set to NULL.
void SDL_FreeTileset(SDL_Tileset *tileset);

//Sets the colorkey on all the images in a SDL_Tileset.
void SDL_SetColorKey(SDL_Tileset *tileset, SDL_Color color);

//Gets a individual tile from the tileset, and returns it.
//Returns NULL if out of bounds.
SDL_Surface *SDL_GetTile(SDL_Tileset *tileset, int tile);

//Draws the tile number 'tile' in the tileset 'tileset' at the location 'x' and 'y' on the surface 'destination'.
void SDL_DrawTile(int x, int y, SDL_Tileset *tileset, int tile, SDL_Surface *destination);

#endif /* SDL_TILESET_H */



SDL_tilesets.cpp:
#include "SDL_tileset.h"

//The first parameter is the name of the large tileset to load.
//The second and third parameters, are the sizes of each small tile. (For example, 32 pixels by 32 pixels)
//The third and fourth parameters allow the user to specify the size of a gap between each tile.
//The fourth and fifth parameters allow the user to the size of a border around the tileset.
SDL_Tileset *SDL_LoadTileset(std::string filename, int tileWidth, int tileHeight, int horizontalSpacing, int verticalSpacing, int horizontalBorder, int verticalBorder)
{
	SDL_Tileset *tileset = new SDL_Tileset; //Our array where we'll stick the broken up surfaces.
	
	SDL_Surface *largeImage = IMG_Load(filename.c_str()); //Load the huge image.
	if(largeImage == NULL) 
	{
		//Oops, something went wrong. Print an error message.
		std::cout << "An error occured in 'LoadTileset()', and we couldn't load the tileset.\n"
				  << "\t" << IMG_GetError() << std::endl;
		
		return tileset; //Return the empty array.
	}
	
	tileset->push_back(largeImage); //The first element in the array is the full image.
	
	//Figure out the size of each tile, including spacing between the tiles.
	int fullTileWidth = tileWidth + horizontalSpacing;
	int fullTileHeight = tileHeight + verticalSpacing;
	
	//This is the width and height of the tileset.
	int tilesetWidth = largeImage->w;
	int tilesetHeight = largeImage->h;
	
	//We are giving the tileset back one of the spaces between the tiles, because we don't want to include
	//the space after the final tile. (We only want the spaces *between* the tiles)
	tilesetWidth += horizontalSpacing;
	tilesetHeight += verticalSpacing;
	
	//We subtract the offsets from the width and height of the tileset, so we don't include any extra
	//spacing the user may want to add to his tileset. (Such as, a border around the edges of the tileset)
	tilesetWidth -= (horizontalBorder / 2);
	tilesetHeight -= (verticalBorder / 2);
	
	//The number of tiles wide, and tiles high, the tileset is.
	int tilesHorizontally = (tilesetWidth / fullTileWidth);
	int tilesVertically = (tilesetHeight / fullTileHeight);
	
	//Loop through each tile horizontally and vertically, and create new images from the huge image.
	for(int y = 0; y < tilesVertically; y++)
	{
		for(int x = 0; x < tilesHorizontally; x++)
		{
			//Figure out where in the large image's pixels, will this tile's pixels be.
			int pixel_offset = ((y * fullTileHeight) + verticalBorder) * largeImage->pitch;
			pixel_offset += ((x * fullTileWidth) + horizontalBorder) * largeImage->format->BytesPerPixel;
			
			//Using the data from the large image, create the new tile.
			SDL_Surface *newTile = SDL_CreateRGBSurfaceFrom(((char*)largeImage->pixels + pixel_offset), tileWidth, tileHeight,
															largeImage->format->BitsPerPixel, largeImage->pitch, 
															largeImage->format->Rmask, largeImage->format->Gmask,
															largeImage->format->Bmask, largeImage->format->Amask);
			
			tileset->push_back(newTile); //Add the tile to the tileset.
		}
	}
	
	return tileset;
}

//The first parameter is the name of the spritesheet to load.
//The second parameter is the name of the spritesheet's background color.
//(Note: The background color must not be the same color as your colorkey. You should not use this color anywhere in your sprites)
//SDL_LoadSpritesheet does the same thing as SDL_LoadTileset, but tries to automaticly locate the sprites, by the background color around them.
//There must be at least 1 pixel of background color between every sprite.
//This function is slow! Luckily, you only need to call it once (per sheet). Still, it'd be
//better to just keep your sprites the same size, and load them with SDL_LoadTileset, when possible.
SDL_Tileset *SDL_LoadSpritesheet(std::string filename, SDL_Color backgroundColor)
{
	SDL_Tileset *spriteSheet = new SDL_Tileset; //Our array where we'll stick the broken up surfaces.
	
	SDL_Surface *largeImage = IMG_Load(filename.c_str()); //Load the huge image.
	if(largeImage == NULL) 
	{
		//Oops, something went wrong. Print an error message.
		std::cout << "An error occured in 'SDL_LoadSpriteSheet()', and we couldn't load the sprites.\n"
				  << "\t" << IMG_GetError() << std::endl;
		
		return spriteSheet; //Return the empty array.
	}
	
	spriteSheet->push_back(largeImage); //The first element in the array is the full image.
	
	//We create a copy of the surface before proceeding, because we're going to draw over the copy.
	SDL_Surface *copyImage = SDL_DisplayFormat(largeImage);
	
	if(SDL_MUSTLOCK(copyImage))
        SDL_LockSurface(copyImage);
	
	std::vector<SDL_Rect> spriteRectArray; //Where we'll store the position and size of the images, temporarily.
	SDL_Rect spriteRect;
	
	Uint32 background = SDL_MapRGB(copyImage->format, backgroundColor.r, backgroundColor.g, backgroundColor.b);
	
	Uint32 *pixels = (Uint32*)copyImage->pixels;
	
	int pixelPitch = (copyImage->pitch / copyImage->format->BytesPerPixel);
	
	//Loop through each pixel, looking for pixels that are not the background color.
	for(int y = 0; y < copyImage->h; y++)
	{
		for(int x = 0; x < copyImage->w; x++)
		{
			//Check if the pixel doesn't match the background color.
			if(pixels[(y * pixelPitch) + x] != background)
			{
				//If it doesn't match, we now have the x and y position of the sprite.
				spriteRect.x = x;
				spriteRect.y = y;
				
				int endY = y;
				
				//Check every pixel vertically, until we find a pixel that DOES match the background.
				while(pixels[(endY * pixelPitch) + x] != background)
				{
					endY++;
					if(endY == largeImage->h)
						break;
				}
				
				//The pixel found, is the end of the sprite vertically. That's the height of the sprite.
				spriteRect.h = endY - spriteRect.y;
				
				int endX = x;
				
				//Check every pixel vertically, until we find a pixel that matches the background.
				while(pixels[(y * pixelPitch) + endX] != background)
				{
					endX++;
					if(endX == largeImage->w)
						break;
				}
				
				//The pixel found, is the end of the sprite vertically. That's the height of the sprite.
				spriteRect.w = endX - spriteRect.x;
				
				//We now have the full rectangle, so push that onto the vector.
				spriteRectArray.push_back(spriteRect);
				
				//Since we know the width of the sprite, we can skip ahead by that amount.
				x += spriteRect.w;
				
				//Finally, draw a rectangle over the sprite we just found, filling it with the background color.
				//(This way, we don't try to get the same image twice)
				SDL_FillRect(copyImage, &spriteRect, background);
			}
		}
	}
	
	
	if(SDL_MUSTLOCK(copyImage))
        SDL_UnlockSurface(copyImage);
	
	//Free the copy of the pixels we made.
	SDL_FreeSurface(copyImage);
	
	
	//Loop through each SDL_Rect, and create new images from the huge image.
	for(int i = 0; i < (int)spriteRectArray.size(); i++)
	{
		//Figure out where in the large image's pixels, will this tile's pixels be.
		int pixel_offset = (spriteRectArray[i].y * largeImage->pitch);
		pixel_offset += (spriteRectArray[i].x * largeImage->format->BytesPerPixel);
		
		//Using the data from the large image, create the new sprite.
		SDL_Surface *newTile = SDL_CreateRGBSurfaceFrom(((char*)largeImage->pixels + pixel_offset), spriteRectArray[i].w, spriteRectArray[i].h,
														largeImage->format->BitsPerPixel, largeImage->pitch, 
														largeImage->format->Rmask, largeImage->format->Gmask,
														largeImage->format->Bmask, largeImage->format->Amask);
		
		spriteSheet->push_back(newTile); //Add the sprite to the sprite sheet.
	}
	
	return spriteSheet;
}

//Frees the SDL_Tileset passed to it. The pointer will be set to NULL.
void SDL_FreeTileset(SDL_Tileset *tileset)
{
	//Loop through each image in the tileset, starting with the
	//large image at the bottom, and free them.
	for(int i = 0; i < (int)tileset->size(); i++)
	{
		SDL_FreeSurface((*tileset)[i]);
	}
	
	//Delete the tileset itself.
	delete tileset;
	
	//Set the tileset to NULL, so we'll know if we accidentally use it in the future.
	tileset = NULL;
}

//Sets the colorkey on all the images in a SDL_Tileset.
void SDL_SetColorKey(SDL_Tileset *tileset, SDL_Color color)
{
	//Loop through each image in the tileset.
	for(int i = 0; i < (int)tileset->size(); i++)
	{
		//Get the individual tile from tileset.
		SDL_Surface *tile = (*tileset)[i];
		
		//Set the colorkey on that tile.
		Uint32 key = SDL_MapRGB(tile->format, color.r, color.g, color.b);
		SDL_SetColorKey(tile, SDL_SRCCOLORKEY, key);
	}
}

//Gets a individual tile from the tileset, and returns it.
SDL_Surface *SDL_GetTile(SDL_Tileset *tileset, int tile)
{
	if(tile < (int)tileset->size())
		return (*tileset)[tile];
	
	return NULL;
}

//Draws the tile number 'tile' in the tileset 'tileset' at the location 'x' and 'y' on the surface 'destination'.
void SDL_DrawTile(int x, int y, SDL_Tileset *tileset, int tile, SDL_Surface *destination)
{
	//Make sure the destination isn't NULL.
	if(destination == NULL)
	{
		std::cout << "An error occured in 'SDL_DrawTile()', and we couldn't draw the tile.\n"
				  << "\t" << "The destination surface we were trying to draw to was NULL." << std::endl;
		
		return;
	}
	
	//Make sure the tileset isn't NULL.
	if(tileset == NULL)
	{
		std::cout << "An error occured in 'SDL_DrawTile()', and we couldn't draw the tile.\n"
				  << "\t" << "The tileset we were trying to draw a tile from was NULL." << std::endl;
		
		return;
	}
	
	//Make sure we aren't going out of bounds.
	if(tile >= (int)tileset->size())
	{
		std::cout << "An error occured in 'SDL_DrawTile()', and we couldn't draw the tile.\n"
				  << "\t" << "Tried to draw tile '" << tile << "' when the tileset only has '" << tileset->size() << "' tiles" << std::endl;
		
		return;
	}
	
	//Fill the SDL_Rect with the position we wish to draw to.
	SDL_Rect drawRect;
	drawRect.x = x;
	drawRect.y = y;
	
	//Draw the tile, and check for errors.
	if(SDL_BlitSurface((*tileset)[tile], NULL, destination, &drawRect) == -1)
	{
		std::cout << "An error occured in 'SDL_DrawTile()', and we couldn't draw the tile.\n"
				  << "\t" << SDL_GetError() << std::endl;
		
		return;
	}
}



Here's the example program:
#include "SDL_tileset.h"

int main(int argc, char *argv[])
{
	SDL_Init(SDL_INIT_EVERYTHING);
	
	SDL_Surface *screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE);
	SDL_WM_SetCaption("SDL_Tileset example", NULL);
	
	//Load the tileset.
	SDL_Tileset *tileset = SDL_LoadTileset("tileset.png", 50, 50, 1, 3, 3, 2);
	
	SDL_Color backgroundColor;
	backgroundColor.r = 255; backgroundColor.g = 255; backgroundColor.b = 255;
	SDL_Tileset *spritesheet = SDL_LoadSpritesheet("spritesheet.png", backgroundColor);
	
	SDL_Color colorkey;
	colorkey.r = 0; colorkey.g = 255; colorkey.b = 255;
	SDL_SetColorKey(spritesheet, colorkey);
	
	SDL_ShowCursor(SDL_DISABLE);
	
	bool quit = false;
	
	while(!quit)
	{
		SDL_Event event;
		while(SDL_PollEvent(&event))
		{
			if(event.type == SDL_QUIT)
			{
				quit = true;
			}
		}
		
		//Erase the screen.
		SDL_FillRect(screen, NULL, 0x00);
		
		//Draw some tiles.
		SDL_DrawTile(0, 0, tileset, /* Tile # */ 1, screen);
		SDL_DrawTile(20, 20, tileset, /* Tile # */ 2, screen);
		SDL_DrawTile(40, 40, tileset, /* Tile # */ 3, screen);
		SDL_DrawTile(60, 60, tileset, /* Tile # */ 4, screen);
		SDL_DrawTile(80, 80, tileset, /* Tile # */ 5, screen);
		SDL_DrawTile(100, 100, tileset, /* Tile # */ 6, screen);
		
		//Draw some more tiles.
		SDL_DrawTile(400, 250, tileset, /* Tile # */ 7, screen);
		SDL_DrawTile(400, 300, tileset, /* Tile # */ 8, screen);
		SDL_DrawTile(450, 250, tileset, /* Tile # */ 8, screen);
		SDL_DrawTile(450, 300, tileset, /* Tile # */ 7, screen);
		
		//Draw some sprites.
		SDL_DrawTile(560, 20, spritesheet, /* Tile # */ 3, screen);
		SDL_DrawTile(100, 300, spritesheet, /* Tile # */ 4, screen);
		SDL_DrawTile(90, 290, spritesheet, /* Tile # */ 5, screen);
		SDL_DrawTile(120, 310, spritesheet, /* Tile # */ 6, screen);
		
		//Draw the mouse, as an animated sprite.
		int mouseX, mouseY;
		SDL_GetMouseState(&mouseX, &mouseY);
		
		//Cheap animation.
		if((SDL_GetTicks() % 500) < 250)
			SDL_DrawTile(mouseX, mouseY, spritesheet, /* Tile # */ 1, screen);
		else
			SDL_DrawTile(mouseX, mouseY, spritesheet, /* Tile # */ 2, screen);
		
		//Update the screen.
		SDL_Flip(screen);
		
		SDL_Delay(50);
	}
	
	SDL_ShowCursor(SDL_ENABLE);
	
	//Free the spritesheet.
	SDL_FreeTileset(spritesheet);
	
	//Free the tileset.
	SDL_FreeTileset(tileset);
	
	SDL_Quit();
	
	return 0;
}



Or, if you prefer to download the code, poorly drawn images included, you can do so here: http://www.mediafire.com/?mld1dkm2zrm

(Requires SDL and SDL_image to run. I assume if you are interested in this, you already know how to use SDL, so I didn't bother including the DLLs)

So, next time you are working on a SDL tile-based game, no need to rewrite the code to separate the tilesets, for the hundredth time. [smile]
It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


Sponsor:



Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS