Python Forum
[PyGame] Loading images, transparency, handling spritesheets (part 2)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Loading images, transparency, handling spritesheets (part 2)
#1
Back to Part 1
https://python-forum.io/Thread-PyGame-Cr...dow-part-1


Download this image
   
import pygame as pg
 
pg.init()
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
image = pg.image.load('spaceship.png').convert()
done = False
while not done:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
    screen.blit(image, screen_rect.center)
    pg.display.update()
This code adds 3 more lines than the previous tutorial of opening a window.

screen_rect = screen.get_rect()
image = pg.image.load('spaceship.png').convert()
Here we create a pygame.Rect from the screen. It allows us to use pygame rects to position things on the screen, instead of hard coding an x and y position (100,100). Although you can if you would like floats as rects convert to ints for speed. More info on that here. After that we load the image with pygame.image.load. You should always use convert() on your image loads as it speeds things up. If you have an alpha channel in your image you should then always use convert_alpha(). More info on alpha channels here and here.

screen.blit(image, screen_rect.center)
Here we blit the image to the screen at the position of the screen center. It puts the top left of the image at the screen center. To do this properly centered, you need to have the image have a rect itself. But that will be for later.

There are a few points to remember.
  • Linux is case sensitive. So if your image is labeled as spaceship.PNG and you load it as spaceship.png. (notice the caps) This means, if you write a program in Windows and give it to your friends in linux, they are going to get an error while you do not.
  • Always use convert() on an image load or convert_alpha() on one with an alpha channel.
  • Don't load your images within your main game loop, or even inside a class' __init__ that will be created numerous times. What is going to happen is your are going to drag your game to a dead stop and it will lag. It will be better explained in part 6 with an example.
  • Images loaded are associated with the main root file executed, not imported modules. (check at the end for more info)
  • When you need help, provide your resources (images, etc.) by making a github repo and uploading all of your code to it. The other alternative is to modify your code to remove the actual image in place of a general pygame surface. Example below.

import pygame as pg
  
pg.init()
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
#image = pg.image.load('spaceship.png').convert()
image = pg.Surface([50,50]).convert()
image.fill((255,0,0))
done = False
while not done:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
    screen.blit(image, screen_rect.center)
    pg.display.update()
This removes any required resources while still getting your point across. When people can actually run your game to tweak it themselves, you have a higher chance of someone helping you. This is also helpful to use in development. Sometimes you dont even need the image and can use a basic rectangle as a placeholder. In that case a general pygame surface will suffice.

Now lets move on and remove that pink background. This is common to color the background pink or (255,0,255) to colorkey it. Just as long as no other part of the actual image has that color. You can use any color, but this is the common one used.

image.set_colorkey((255,0,255))
By adding this line after loading the image, you can make the pink background transparent. It sets the colorkey to the background color, making it invisible.

Now lets organize this code better. The more we add to the image and modifying it, the more messy our code is getting. We need to separate the player (the image, and its data and logic). So we are going to make a player class to house this image and its data in. Don't freak out if you never used classes before. We are going to start simple and just convert our current code to use a class. And then instead of adding on to it in global scope, we will add on to it in the class.

import pygame as pg
 
pg.init()
 
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('spaceship.png').convert() #create player.image attribute
        self.image.set_colorkey((255,0,255))                  
        self.rect = self.image.get_rect(center=screen_rect.center) #create player.rect attribute from the image and position it to screen center
         
    def draw(self, surf):
        surf.blit(self.image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect) #create player object, run __init__ (dunder init method)
done = False
while not done:
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    pg.display.update()
So here we made a class for the player. The only thing we really added was the rect for the player. It gets the size of the rect from the size of the image. Also you can give it a position argument. Here it is centered on the screen center. We made a draw method to house the actual blit contents for the image and we draw it at the position of its rect. Now in the main game loop we just call this method and pass the screen to it.

Pygame rects are built-in to eliminate the need for x and y positions. It has built-in collision methods and variables to handle every possible need in a game. Every image will have one. Every image will follow their assigned rect. The rects are what you use to handle movement of the image, collision with other rects, etc. In the above example, the image is blit to the location of the rect. We assigned the starting location of that rect to the center of the screen. So hence the image is going to be in the center of the screen. If we move the rect with keyboard input, the image will move along with it. Pygame rects are a very important part of pygame that removes redundant code. Learning to use them would be wise.

Don't know classes? Im going to tell you now. If you dont know classes and have been putting learning them off. Now is a good time to learn. We have an assortment of class tutorials. Your going to want to at least be familiar with the basics and inheritance. Classes help organize code from being spaghetti code. They help you when you come back to a game in a couple years, and they help others who help you when you get stuck figure out things. They simplify things believe it or not. For example you can inherit your objects from pygame.sprite.Sprite and run pygame collide_mask with 2 different masks to see if they collide or now instead of doing the math yourself. Or use pygame.sprite.Group so you dont have to blit every single image manually, you just blit the groups of images, etc. Classes also makes things simple by removing things like global keywords sprinkled all over your code. You wont need those as class variables as they can be accessed/changed from all of that class' methods. You dont have to pass args in and out of functions anymore, etc.

A quick run down of classes in this example. If you look at this example from the previous (aside from making player.rect attribute), its all just copied over into the class. Instead of creating a global variable image we house that variable in the player class which would be self.image. A more in depth tutorial of self here. And then we moved drawing that image to the draw method of that class and replaced it with player.draw(screen) to execute that draw method. It might seem a little ridiculous now, but as you add more and more code pertaining to the player, it makes sense. You dont want to clutter the main game loop with player stuff...and what happens when we have enemies too? It organizes the code.

Lets move on. You can always stop here and do some testing to better understand classes. Ask questions here on the forum. We are welcome to any question. Nothing is too stupid. We understand....we ALL were there at one point and remember how hard it is to grasp the concept. From this point ill assume you have a basic understanding of classes.

We are going to rotate this image 180 degrees before we call it quits. It as is faces downwards. And we are going to face it upwards. Whenever you scale or rotate, you MUST always not use the original image. This is mainly for if you are constantly rotating the image by key press (like a gun turret). But its a good habit to get into. It will save you a headache down the line.

self.transformed_image = pg.transform.rotate(self.image, 180)
This line takes the original image (self.image) and rotates it 180 degrees, then saves it as a new surface. Notice i did not save it back into self.image, but a new variable. This is vital in a constant rotating image, but not here.

surf.blit(self.transformed_image, self.rect)
The other thing we changed was to blit the new rotated image and not the original

import pygame as pg
  
pg.init()
  
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        self.rect = self.image.get_rect(center=screen_rect.center)
          
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
done = False
while not done:
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    pg.display.update()
Download this image


This image has an alpha channel. The background is transparent. This means we must load the image with convert_alpha() instead. Im going to show you how to load and color this image to a different color.

import pygame as pg
  
pg.init()
 
def colorize(image, newColor):
    """
    Create a "colorized" copy of a surface (replaces RGB values with the given color, preserving the per-pixel alphas of
    original).
    :param image: Surface to create a colorized copy of
    :param newColor: RGB color to use (original alpha values are preserved)
    :return: New colorized Surface instance
    """
    image = image.copy()
 
    # zero out RGB values
    image.fill((0, 0, 0, 255), None, pg.BLEND_RGBA_MULT)
    # add in new RGB values
    image.fill(newColor[0:3] + (0,), None, pg.BLEND_RGBA_ADD)
 
    return image
  
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('chevron.png').convert_alpha()
        self.image = colorize(self.image, (255,0,0))
        self.rect = self.image.get_rect(center=screen_rect.center)
          
    def draw(self, surf):
        surf.blit(self.image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
done = False
while not done:
    screen.fill((255,255,255))
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    pg.display.update()
So here we have the exact same code as before. We updated the image to load this chevron instead. As well as make it load via convert_alpha() instead of just convert(). The only difference in the class is we execute the new function colorize on the image. This function shades a new color on the image. In this example we change the original black chevron to a red chevron.

   
This example shows a rainbow in which changes a specifc color to transparent..in this case the green color. We swap the green color with our colorkey using PIxelArray, and then set the colorkey to make it transparent. 
import pygame as pg
   
pg.init()
  
COLORKEY = (0,0,0) #black

RED =    (221,0,0)
ORANGE = (254,98,98)
YELLOW = (254,246,0)
GREEN =  (0,187,0)
BLUE =   (0,155,254)
INDIGO = (0,0,131)
VIOLET = (48,0,155)
    
def swap_color(surf, from_, to_):
    arr = pg.PixelArray(surf)
    arr.replace(from_,to_)
    del arr
   
class Rainbow:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('rainbow.png').convert()
        self.rect = self.image.get_rect(center=self.screen_rect.center)
        swap_color(self.image, (0,187,0), COLORKEY)
        self.image.set_colorkey(COLORKEY)
           
    def draw(self, surf):
        surf.blit(self.image, self.rect)
   
screen = pg.display.set_mode((1920,1080))
screen_rect = screen.get_rect()
player = Rainbow(screen_rect)
done = False
while not done:
    screen.fill((0,0,255))
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    pg.display.update()
Output:
   
Now we are going to make something partially transparent. In this case we are going to use the previous code of the rainbow, and just make an overlay overtop of the entire screen. This would be the equivalent of something like an in-game menu system or pause features for example.
import pygame as pg
   
pg.init()
  
COLORKEY = (0,0,0) #black

RED =    (221,0,0)
ORANGE = (254,98,98)
YELLOW = (254,246,0)
GREEN =  (0,187,0)
BLUE =   (0,155,254)
INDIGO = (0,0,131)
VIOLET = (48,0,155)
    
def swap_color(surf, from_, to_):
    arr = pg.PixelArray(surf)
    arr.replace(from_,to_)
    del arr
   
class Rainbow:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('rainbow.png').convert()
        self.rect = self.image.get_rect(center=self.screen_rect.center)
        swap_color(self.image, (0,187,0), COLORKEY)
        self.image.set_colorkey(COLORKEY)
           
    def draw(self, surf):
        surf.blit(self.image, self.rect)
        
class Overlay:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((self.screen_rect.width, self.screen_rect.height))
        self.image.fill(0)
        self.image.set_alpha(200)
        
    def draw(self, surf):
        surf.blit(self.image, (0,0))
   
screen = pg.display.set_mode((1920,1080))
screen_rect = screen.get_rect()
player = Rainbow(screen_rect)
overlay = Overlay(screen_rect)
done = False
while not done:
    screen.fill((255,255,255))
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    overlay.draw(screen)
    pg.display.update()
Nothing has changed from the previous code except we are creating a darkened somewhat transparent surface overtop of the rainbow. 
Output:
   
[anchor=spritesheet][/anchor]
Download this image
   

Now we are going to teach you how to load a spritesheet. Instead of loading each image one after the other we can just load a sheet of images once and cut them into frames. The pros are the spritesheets tend to be smaller files. Spritesheets load quicker as there is one disk acess rather than several. Less wasted video mem, etc. At some point or another you are going to have to use a spritesheet.

There are two main different types of spritesheets. The one shown is uniformed. Each image is of the same size, such as a grid. This is ideal to have them this way as you can just loop through and cut the images.However sometimes images must be of different sizes and cannot be uniformed like this. In this case you must define the rectangle to cut by pixel instead for each image.

So we are going to start by loading the dice image. Our end result is inteded to be a list of pygame surfaces that are already loaded. So the_list[0] would be die 1, the_list[1] would be die 2, so on and so on. And by just blitting the single index would draw that die to the screen.

import pygame as pg
 
def strip_from_sheet(sheet, start, size, columns, rows):
    frames = []
    for j in range(rows):
        for i in range(columns):
            location = (start[0]+size[0]*i, start[1]+size[1]*j)
            frames.append(sheet.subsurface(pg.Rect(location, size)))
    return frames
 
pg.init()
   
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
done = False
 
sheet = pg.image.load('dice.png')
dice = strip_from_sheet(sheet, (0,0), (36,36), 1, 6)
 
while not done:
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    screen.blit(dice[0], screen_rect.center)
    pg.display.update()
The strip_from_sheet function assumes that your sheet is uniformed. It loops through the rows and columns and locates each die indicated by the args, and creates a pygame.subsurface for each die. Because the sheet is uniformed, the process in which it loops through each image is like looping through a nested list. Just in this case the nested lists are single as there is only one row. So where we execute this function we give a number of arguments. the (0,0) is the first starting location. Every loop it is the topleft of the subsurface. Starting off initially, it is the topleft of the entire sheet. the (36,36) is the size of each image. You can manually locate this info by dividing the number of images across by the number of pixels the sheet is wide. This gives you the number of frames horizontally. You can do the same for getting the number of frames vertically. Another way to determine is to check the image in GIMP. GIMP is a free photo editing tool, like PhotoShop. I would always check images in GIMP that you obtain from the internet to ensure that the sheet is correctly aligned before assuming it should work. If not you can easily modify it in GIMP as well as create your sprites in GIMP. GIMP itself is complex and needs a whole other tutorial for each tool within. The next argument is the number of columns in the sheet, which is 1. And then the last argument is the number of rows, which is 6.

The dice variable is a list returned from strip from sheet function. Inside is a pygame surface for each die. We blit the first die in the main game loop. You can change this to each die by just change the index of dice being blitted. So change it to 1, and you will see die 2 draw and so on. You could make it loop through each die, but that would require more code such as a timer (how long between flips and when to flip) etc. We are going to leave that for another tutorial.

You can easily load 1/2 of an image or an image by thirds in the same manner. Just pretend that the single image is a spritesheet with two images, one on the left, and one on the right. Instead of giving the individual frames sizes to the same funcition, you would just get the size of the image, and divide it by 2 to get half of the image

size = sheet.get_size()
frames = strip_from_sheet(sheet, (0,0), (size[0]/2,size[1]), 2,1)
This will get the size of the spritesheet. The second line is similar to the previous call to strip_from_sheet. However we just give the dimensions of the image, dividing the width by 2 to get half. And give it 2 column (two halfs) and 1 row. We could also do this in thirds and across horizontally by dividing by 3 instead and changing columns to 3. Or we could flip these and do the top and bottom half by doing...

frames = strip_from_sheet(sheet, (0,0), (size[0],size[1]/2), 1,2)
This is just to show you that you dont really need to edit the function strip_from_sheet. You can basically copy and paste it into all your games, and just change the arguments to fit every uniformed spritesheet you have.

The alternative is to load the coordinates of the image. This is used for if the frames within the spritesheet are not uniformed. Sometimes there are spritesheets that try to fill every available space in the spritesheet or keep similar sprites in the same sheet, which all can result in uninifirmed images on the spritesheet. You have to going to have to manually insert each coordinates for each image in that case. If the majority are uniformed and one is not, you can make it easier by loading all as uniformed and set which ones are not used, and load the odd ball separately, for example. But if every image is of a different size, it could be a lot of work to get all the coordinates of the images, especially if you just pull a sheet off the internet.

def strip_coords_from_sheet(sheet, coords, size):
    frames = []
    for coord in coords:
        location = (coord[0]*size[0], coord[1]*size[1])
        frames.append(sheet.subsurface(pg.Rect(location, size)))
    return frames
Here sheet is your image, coords is a list of topleft positions of each image relative to the spritesheet, and size is a list of each image for how big each image is. These two lists must the same length. The first element of each list corresponds with one sprite, the second element corresponds to another, etc. It spits out all your frames. You will have to manually go in with GIMP and inspect each and every sprite coordinates in relation to the spritesheet. I would also run a test script to make sure you got the correct coordinates before assuming they should work.

As you can see it is much easier to load uniformed spritesheets like the dice. This is ideal, but not always available.


Loading image problems:

The image must be loaded in association with the main file executed. If a main file imports a module, and the module loads an image and the image is lets say in the directory of that imported module. The load will fail if you simply just load the image file name without a path because pygame is thinking you are trying to load the image from the directory where the main file is. Even though the module is the one that is loading it.
Example:
    my_game/
        main.py
        data/
            loader.py
            spaceship.png
main.py
from data import loader
loader.py
import pygame
pygame.init()
img = pygame.image.load('spaceship.png')
Output:
metulburr@ubuntu:~/testerer$ python3 main.py Traceback (most recent call last): File "main.py", line 1, in <module> from data import loader File "/home/metulburr/my_game/data/loader.py", line 3, in <module> img = pygame.image.load('spaceship.png') pygame.error: Couldn't open spaceship.png
To fix this you can either
1) move the image to the root directory
2) change the image load path to '../spaceship.png'
3) or setup an image directory and create a load function to globally use everywhere as to not make hard coded paths everywhere in your game





Part 3
https://python-forum.io/Thread-PyGame-Ba...ing-part-3
Recommended Tutorials:
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] Basic event handling (part 3) metulburr 0 7,500 Oct-09-2016, 03:15 PM
Last Post: metulburr

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020