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 set_mode()
function: 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 HWSURFACE
and 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()
The get_width()
and 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 True
.
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()
The 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: renderRatio
and 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)
Note the 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 rescaledSurfaceX
and 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.