This article covers the concept of level generation in pygame.
Level generation is a tough concept to implement perfectly in Pygame or any game engine really. Most people manually try to create their levels by defining each level layout individually. It’s OK in some adventure game which has a set number of maps or levels, however in a Platformer game this is very limiting because there should not be a limit to our map.
This can vary from game to game, but random level generators are one of the most powerful types of level generation in pygame. If implemented correctly once, you have yourself an infinite game.
The concepts we’re going to go over today can be reused in a variety of situations. For instance, maze games where you require the maze to be generated at random, or a game like flappy bird where you want to randomize the obstacles in your path.
Part 2 – Code
Here’s the code from the previous section as reference. We’ll simply be adding the Level Generation code to this, so be sure to go through this once again.
import pygame
from pygame.locals import *
import sys
import random
pygame.init()
vec = pygame.math.Vector2 #2 for two dimensional
HEIGHT = 450
WIDTH = 400
ACC = 0.5
FRIC = -0.12
FPS = 60
FramePerSec = pygame.time.Clock()
displaysurface = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Game")
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
#self.image = pygame.image.load("character.png")
self.surf = pygame.Surface((30, 30))
self.surf.fill((128,255,40))
self.rect = self.surf.get_rect()
self.pos = vec((10, 360))
self.vel = vec(0,0)
self.acc = vec(0,0)
def move(self):
self.acc = vec(0,0.5)
pressed_keys = pygame.key.get_pressed()
if pressed_keys[K_LEFT]:
self.acc.x = -ACC
if pressed_keys[K_RIGHT]:
self.acc.x = ACC
self.acc.x += self.vel.x * FRIC
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
self.rect.midbottom = self.pos
def jump(self):
hits = pygame.sprite.spritecollide(self, platforms, False)
if hits:
self.vel.y = -15
def update(self):
hits = pygame.sprite.spritecollide(P1 ,platforms, False)
if P1.vel.y > 0:
if hits:
self.vel.y = 0
self.pos.y = hits[0].rect.top + 1
class platform(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.surf = pygame.Surface((WIDTH, 20))
self.surf.fill((255,0,0))
self.rect = self.surf.get_rect(center = (WIDTH/2, HEIGHT - 10))
def move(self):
pass
PT1 = platform()
P1 = Player()
all_sprites = pygame.sprite.Group()
all_sprites.add(PT1)
all_sprites.add(P1)
platforms = pygame.sprite.Group()
platforms.add(PT1)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
P1.jump()
displaysurface.fill((0,0,0))
P1.update()
for entity in all_sprites:
displaysurface.blit(entity.surf, entity.rect)
entity.move()
pygame.display.update()
FramePerSec.tick(FPS)
Initial Level Generation
We’re going to divide our level generation code into two parts. The first part is in-charge of generating the platforms that appear in the start of the game. This code will only run once, outside of the Game loop.
for x in range(random.randint(5, 6)):
pl = platform()
platforms.add(pl)
all_sprites.add(pl)
The code we wrote above will run either 5 or 6 times. This is a little extra feature we added in. Puts a more random element into the game.
The next three lines simply create between 5 – 6 different platforms. Since we just changed the platform class code, the generated platforms will have their own random positions on the screen.
Updating the Platform Class
The setup of the platform class uptil now has just been a temporary solution. We now need to expand and improve upon it to be able to handle random level generation.
class platform(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.surf = pygame.Surface((random.randint(50,100), 12))
self.surf.fill((0,255,0))
self.rect = self.surf.get_rect(center = (random.randint(0,WIDTH-10),
random.randint(0, HEIGHT-30)))
The above code creates a platform of random width from 50 – 100 pixels. The code itself, is pretty simple. First we created a surface, and then use the get_rect()
to define it’s borders. Using the center parameter we defined the position of where we want the platform to appear.
Of course, now that we’ve changed the platform class, we’re going to have to create separate code for Platform 1 as shown below.
PT1.surf = pygame.Surface((WIDTH, 20))
PT1.surf.fill((255,0,0))
PT1.rect = PT1.surf.get_rect(center = (WIDTH/2, HEIGHT - 10))
Infinite Scrolling Screen
Now we’re going to create the infinite scrolling screen. It’s going to be a bit different from the traditional type of scrolling backgrounds where the background just keeps moving endlessly, like the one we made in our Beginner Pygame Tutorial.
Before we proceed, you should understand our goal. We must create such a feature that allows our character to move up indefinitely. Imagine a game, where you keep jumping on platforms. As you move up, the screen moves with you as well. Our game lacks this feature currently.
The following code will be inserted into the Game Loop.
if P1.rect.top <= HEIGHT / 3:
P1.pos.y += abs(P1.vel.y)
for plat in platforms:
plat.rect.y += abs(P1.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()
Due to it’s complexity and importance we’ll examine this line by line.
if P1.rect.top <= HEIGHT / 3:
This an importance concept to understand. It checks to see the position of the player with respect to the screen. This is basically the deciding point for when to move the screen up. After some testing with our screen size, player speed etc, we decided on setting that point for HEIGHT / 3
.
Every time the Player’s position reaches the HEIGHT / 3
point, the code within this if statement
will execute.
P1.pos.y += abs(P1.vel.y)
Since our screen is no longer an dynamic object, we have to keep updating the position of the player as the screen moves. We use the abs() function to remove the negative sign from the velocity value.
for plat in platforms:
plat.rect.y += abs(P1.vel.y)
We’ve updated the position of the player, but we have to do the same for every other sprite on the screen. Here we iterate through all the platforms in the platform group and update their position as well.
if plat.rect.top >= HEIGHT:
plat.kill()
This destroys any platforms that go off the screen from the bottom. If it weren’t for this line, the number of platforms in memory would keep accumulating, slowing down the game.
Note: All we really did was to move the platforms and player sprites down, we didn’t actually do anything to the screen. Even if we had a background, we still wouldn’t have to do anything other than what we did above.
Random Level Generation
This is the main code behind the random level generation concept. It’s in-charge of generating “future” platforms. In simpler words, it creates platforms above the screen. When the player moves up, the screen shifts and these platforms become visible.
We generate the platforms off-screen because it would be very awkward to see the platforms actually appear on the main screen.
def plat_gen():
while len(platforms) < 7 :
width = random.randrange(50,100)
p = platform()
p.rect.center = (random.randrange(0, WIDTH - width),
random.randrange(-50, 0))
platforms.add(p)
all_sprites.add(p)
Once again, we’ll explain this line by line.
while len(platforms) < 7 :
The reasoning behind this line is a bit hard to explain, but i’ll do my best. It’s something I discovered after hours of testing and fixes. If you noticed, in the code before, we only initialized 5 – 6 platforms. Yet, here we have 7. This code is supposed to run only when there are less than 7 platforms on screen.
It’s necessary for this value to be greater than the one we gave for the initial platform generation (5 – 6). Why? You can try running this for yourself to understand better, but i’ll try to sum it up quickly.
If this value is not greater than the initial number of platforms, a new platform will not generate until the player has reached the HEIGHT / 3
on the screen. The result is that there will be a large gap between the player and the next platform that we will be unable to cover in a single jump.
width = random.randrange(50,100)
Assigns a random width which we’ve use to create new platforms. Added for a degree of variety in our platforms.
p.rect.center = (random.randrange(0, WIDTH - width),
random.randrange(-50, 0))
platforms.add(p)
all_sprites.add(p)
Creates and places the platform right above the visible part of the screen. The position is randomly generated using the random library.
Finally the platform is added to both sprite groups.
Of course, we also have to add the plat_gen()
function call in the game loop, so don’t forget that.
A short video displaying our progress uptil this point. It looks pretty good, despite being pretty rough around the edges right now.
Take careful note of any problems you see in the game. We’ll be addressing them in the next section.
Now that we’ve added Level generation and the infinite scrolling screen, we’re done with our main core of our game. However, there are still many things lacking, and many improvements to be made.
Take the next section very seriously. It’s very easy to make a game like the one we just made. It’s much harder however, to improve on that game, fix it’s bugs and errors and genuinely make a fun and engaging game to play. You won’t be able to achieve any of this if you don’t read the next section.
This marks the end of the Pygame Level Generation article. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the article material can be asked in the comments section below.