In the Discover Python and Patterns series, we always create a window whose size depends on the game scene. If the screen resolution is much lower or higher than its size, it leads to a poor user experience. We can solve this issue using the scale function inside Pygame!
This post is part of the 2D Strategy Game series
As you can see in this video, whatever the size of the window, the resolution of the game scene is always the same. We also add black borders to keep the aspect ratio:
We update the previous program in the following way:
import pygame from pygame.constants import HWSURFACE, DOUBLEBUF, RESIZABLE from pygame.surface import Surface pygame.init() # Load image and create window with default resolution window = pygame.display.set_mode((1024, 768), HWSURFACE | DOUBLEBUF | RESIZABLE) pygame.display.set_caption("2D Medieval Strategy Game with Python, http://www.patternsgameprog.com") # The size of our game scene is the one of the image image = pygame.image.load("toen/screen_cap_2.png") renderWidth = image.get_width() renderHeight = image.get_height() running = True while running: # Handle input for event in pygame.event.get(): if event.type == pygame.QUIT: running = False break elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False break # Render scene in a surface renderSurface = Surface((renderWidth, renderHeight)) renderSurface.blit(image, (0, 0)) # Scale rendering to window size windowWidth, windowHeight = window.get_size() renderRatio = renderWidth / renderHeight windowRatio = windowWidth / windowHeight if windowRatio <= renderRatio: rescaledSurfaceWidth = windowWidth rescaledSurfaceHeight = int(windowWidth / renderRatio) rescaledSurfaceX = 0 rescaledSurfaceY = (windowHeight - rescaledSurfaceHeight) // 2 else: rescaledSurfaceWidth = int(windowHeight * renderRatio) rescaledSurfaceHeight = windowHeight rescaledSurfaceX = (windowWidth - rescaledSurfaceWidth) // 2 rescaledSurfaceY = 0 # Scale the rendering to the window/screen size rescaledSurface = pygame.transform.scale( renderSurface, (rescaledSurfaceWidth, rescaledSurfaceHeight) ) window.blit(rescaledSurface, (rescaledSurfaceX, rescaledSurfaceY)) pygame.display.update() pygame.quit()
Line 8 creates the window:
window = pygame.display.set_mode((1024, 768), HWSURFACE | DOUBLEBUF | RESIZABLE)
Its size is 1024 per 768 pixels: we can set any value; the following code rescales the game scene to this size.
Note the second argument of the
HWSURFACE | DOUBLEBUF | RESIZABLE. It is the combination of three options for window creation. We use the
| operator to combine them: we want to enable all of them. The
DOUBLEBUF options allow a faster blitting on the screen with video cards that support it (almost all cards today). With the
RESIZABLE option, the user can resize the window.
Note that the full name of Pygame options starts with
pygame.constants.. So, for instance, the full name of the resizable option is
pygame.constants.RESIZABLE. Using an import at the beginning of the program, we can simplify this name:
from pygame.constants import RESIZABLE
Using Pycharm, we don't need to know the full name of library variables or functions. If you type
RESIZABLE without the import, you'll see a red line below this word. Leave the mouse cursor on it, a popup menu appears, and click "Import this name". Then, choose the import you prefer: Pycharm adds it automatically.
Line 12 reads an image: it is the only "sprite" we use in this example. The
pygame.image.load() function loads an image file and returns a surface:
image = pygame.image.load("toen/screen_cap_2.png")
Lines 13-14 define the size of the game scene:
renderWidth = image.get_width() renderHeight = image.get_height()
get_height() methods of the Pygame
Surface class return the width and height of the surface. The size of our game scene is the one of the image; you can try other sizes to see what happens.
Lines 16-53 contain the main game loop. It still runs as long as the
running variable is
Lines 20-27 handles input as before and quit if the user clicks the close button or presses the Escape key.
Lines 30-31 render our scene:
renderSurface = Surface((renderWidth, renderHeight)) renderSurface.blit(image, (0, 0))
The first line creates a new Pygame surface. Its size is the one we chose previously. We can draw inside safely since it always has the same size. The second line is an example of scene rendering: we blit a single image. This rendering is in memory: we have to draw it in the window to see it on the screen.
Lines 34-46 are the most important. They compute the size of our scene in the window without changing its aspect ratio. Otherwise, we could stretch the surface in one direction.
Line 34 gets the width and height of the window:
windowWidth, windowHeight = window.get_size()
get_size() method of the
Surface class returns a tuple of integers with the width and height of the surface. Note that we could also write:
windowWidth = window.get_width() windowHeight = window.get_height()
Lines 35-36 compute the aspect ratios of the scene and window surfaces:
renderRatio = renderWidth / renderHeight windowRatio = windowWidth / windowHeight
Note that the
/ division is a float division:
windowRatio are float values (not integers).
Then, depending on these ratios, the size of the rescaled scene is different.
Lines 37-41 handle the case where the window aspect ratio is smaller or equal to the scene aspect ratio. It means that we can use the full window width but not the full height. Line 38 sets the rescaled width to the window width:
rescaledSurfaceWidth = windowWidth
Line 39 computes a height that leads to a rescaled size with the aspect ratio of the rendering:
rescaledSurfaceHeight = int(windowWidth / renderRatio)
int() function that converts to integers. The scale function in the following expects integer values (not floats), so we make sure that it is the case.
We can do some maths to check that our computation is right:
rescaledAspectRatio = rescaledSurfaceWidth / rescaledSurfaceHeight = windowWidth / (windowWidth / renderRatio) = windowWidth / windowWidth * renderRatio = renderRatio
Lines 40-41 compute the coordinates of the rescaled surface in the window in order to center it:
rescaledSurfaceX = 0 rescaledSurfaceY = (windowHeight - rescaledSurfaceHeight) // 2
This time, we use the
// integer division:
rescaledSurfaceY is an integer.
Lines 42-46 do the same, but for the case where we can use the full height of the window.
Lines 49-51 uses the
pygame.transform.scale() function to rescale the game scene:
rescaledSurface = pygame.transform.scale( renderSurface, (rescaledSurfaceWidth, rescaledSurfaceHeight) )
The first argument is the surface to rescale, and the second one is the target size.
Line 52 blits the rescaled surface on the window/screen. The
rescaledSurfaceY values ensure that we center this surface:
window.blit(rescaledSurface, (rescaledSurfaceX, rescaledSurfaceY))
In the next post, we start the design of the game state.