We will be looking at a bit of theory again in this chapter. First let’s start with images. An image is often used interchangeably with the terms picture and bitmap. If you recall, we talked about bitmaps in chapter 4 — where we understood what a bitmap was. Images are usually stored on a storage medium in an Image File. An image file has image data which is stored in a standardized format called image format. An image format is a specification that defines how image data should be arranged and formatted so that it can be universally read. In in our case it’s SFML that reads all the images. It’s tempting to think images as bitmaps, however not all image formats store data in pixels. Some formats store data in vector graphics format. We won’t be getting into vector graphics images in this series since it’s out of scope for a simple python based game like ours. All the images we will be using will have raster graphics i.e. pixel data — but it’s important to understand the difference and stop thinking images as only bitmaps [1]. For the remainder of this chapter I will only talk about raster graphic images, i.e. bitmap image formats.
Along with storing data, an image format also specifies how the image data is compressed. If you go through the specs of most formats, you will see that they usually have compressed data. That brings us to the question, why should an image be compressed? That’s because without compression an image file would become huge very quickly [2]. A true color image having a alpha channel requires 4 bytes per pixel. So the size of a relatively small image of 1024 by 1024 pixels would become 1024 x 1024 x 4 = 4,194,304 bytes or 4 MB. With compression applied, this size can be brought down significantly. Smaller files are faster to read off the disk and easier to transfer and maintain. But does compression degrade the image data? The answer is — it depends on what image format you use. There are two types of image compression algorithms that are in use, there are the lossy compression algorithms and the lossless compression algorithms. Lossy compression algorithms produce significantly smaller image files at the cost of some decrease in image quality, whereas lossless compressions preserve the quality of the image but produce image files that are a a bit larger than lossy algorithms, but still smaller than uncompressed images. The JPEG family (.jpg, .jpeg, etc) file formats use lossy compression algorithms whereas the PNG format is a lossless image format. Since our game will be a standalone single player game, we will mostly use the PNG format. The PNG format gives a pretty decent compression ratio and allows us to maintain our images in lossless quality. The image format is also a free image format and is supported by SFML.
That was a bit of a long lesson we went through there so now let’s try and see how we handle images in our game. Images are resources — remember resources from chapter 3? Since they are external files, they need to be loaded before we can use them inside our game. SFML takes care of loading images for us and is quite simple to do. We will be using the file squares.png shown in 7.1. The file is a 128 pixels x 128 pixels in size.
The code for loading the file is.
img = sf.Image() img.LoadFromFile('squares.png')
That was easy, but wait, how do we display our image? Well, as I said above, an image is a resource and we must map our resource to a drawable entity before it can be displayed in our view. In SFML terms a drawable entity having an image is called Sprite. In classical game development terms a sprite is usually a 2 dimensional image or an animation which forms a part of the game screen. Several small sprites are integrated, often seamlessly within a game to build a game world. A sprite is a drawable entity, that is often, but not always rectangular. In SFML it’s very easy to create and draw a sprite. Let’s see how it’s done using the image we loaded above.
squaresSprite = sf.Sprite(img) ... ... #In our game loop window.Draw(squaresSprite) ... ...
The above code will display the entire squares.png in our view. But what if we want only part of our image? Say for instance — what if we wanted only the red square? Can we get a part of our image into a sprite? Yes we can. It can be done by setting a sub-rectangle in our sprite. Let’s look at the following piece of code –
redSprite = sf.Sprite(img) redSprite.SetSubRect(sf.IntRect(65, 0, 128, 64))
What does this code do? It sets the co-ordinates of the internal rectangle of the sprite to use only a portion of our image while drawing. Just like we have co-ordinates for the Screen and our View (as we saw in chapter4), our images can also be addressed with the help of co-ordinates — and just like our screen and view our images also have co-ordinate axes. In our code the SetSubRect sets the sub-rectangle to left (x1) = 65pixels, top (y1) = 0 pixels, right (x2) = 128 pixels and bottom (y2) = 64 pixels, which represents the red portion of our image.
There is one question you might ask — Why was a sf.Image() needed? Wouldn’t it have been easy just to load the entire image into a sprite and then draw it? Why do we need an intermediate resource? That’s because, resources are shared. What would happen if we wanted to use 4 sprites that use the same image? Say if we wanted yellow, red, blue and green sprites. We would have had to load the image 4 times, once for each sprite. A terrible waste of memory. Instead of that we load the image only once and create multiple sprites using sub-rects from the same image. When images are loaded, they have to be decompressed and loaded as bitmaps — and we saw how large raw bitmaps can be. It would be unwise for us to load the image 4 more times for 4 other sprites that use the same image. Instead we share the image between sprites.
yellowSprite = sf.Sprite(img) yellowSprite.SetSubRect(sf.IntRect(0, 0, 64, 64)) redSprite = sf.Sprite(img) redSprite.SetSubRect(sf.IntRect(65, 0, 128, 64)) blueSprite = sf.Sprite(img) blueSprite.SetSubRect(sf.IntRect(65, 65, 128, 128)) greenSprite = sf.Sprite(img) greenSprite.SetSubRect(sf.IntRect(0, 65, 64, 128))
This is how resource sharing takes place. Always strive to share resources between game entities. If you have a lot of small sprites, it’s always wise to bunch them up together in one image. There are two advantages to that — a) You will need less image loads (which will be fast) and also require less memory — and b) Graphics rendering hardware is optimized for drawing large batches of entities. When there are a lot of sprites that use the same image, the drawing can be bunched up into one large batch and drawn in a single instance. That increases rendering speed. We will see this later in chapters related to optimizations. Also remember to try and keep the width and height of your image as a power of 2 (16, 32, 64, 128, 256,…). Sometimes graphics hardware can’t support non power of two (NPOT) images. In that case SFML will increase the internal storage size for your image and make it a power of 2, thus wasting space. We used a 128 x 128 pixel image, that is a power of two so no problems there. However, what would have happened if the image was 130 x 130 pixels and if NPOT wasn’t supported? The internal texture where the image data would be loaded would have been scaled to 256 x 256 pixels to accommodate the 130 x 130 pixel image.
With that it’s time to introduce another term, and that term is Texture. In a graphics pipeline a texture is a bitmap (or series of bitmaps), that can be mapped to an object like a triangle. Mapping a texture to an object is called texture mapping. SFML doesn’t use or expose the term texture or texture mapping in it’s APIs directly (, though it internally does use textures). Instead it just uses the term Image (sf.Image) and gives us a mapped object like a Sprite (sf.Sprite). However, texture and texture mapping are accepted and used terms in graphics. The correct term for image co-ordinates that we saw earlier is actually texture co-ordinates. You will see these terms used very frequently when dealing with graphics. We won’t be getting into the depths of textures in this series since it’s out of scope for a small 2D game like ours. However you will see me use the word texture, texture mapping and texture co-ordinates in later chapters.
In the next chapter we will see how we manage and automate the loading and sharing of images in our framework.
Downloads
[1] – Vector Graphic formats are very often used by artists and content creators to create game related art. Usually intricate and detailed art is created in a vector graphics format and then exported or incorporated into the game pipeline by converting into a raster graphics format. The other place where vector graphics formats are used directly is in flash based games — where most of time the images are vector graphics based.
[2] – Uncompressed images are very large. Consider a digital image taken by a 12 Mega-pixel camera. Each pixel without transparency in true color is 3 bytes. So the image taken by the camera would become – 12,000,000 x 3 = 36,000,000 bytes i.e. 36Mb. Now consider that you have an 8 Gb card in your camera. So if the image data were to remain uncompressed, you could at the most have 8000/36 = 222.22 or 222 images on your card. In reality because your camera compresses the image file to a .jpg format, you can fit 10 times that amount.