-
Notifications
You must be signed in to change notification settings - Fork 2
5. Adding Levels and Creating the Menu
To make a second level, go to the settings.py file, and you can be creative on how you want to layout the 2nd level. For simplicity we made our 2nd level in 'level_map2'.
from os import walk, path
import pygame
level_map = [
' ',
' ',
' E ',
' CCC CCC CC CCC CCC CC ',
' P C E C J ',
' CC CCCC EMC CCCC ',
' ECC CC C ECC CC C',
' CCMM CCC M CCMM CCCE M',
' MMMM E MC M MMMM MC M',
' CCCC MMMMCC CC MMCCCM CCCC EMMMMCC CC MMCCCM',
'CCCCMMMMCCMMMMMMCCMMCCMMMMMMCCCCMMMMCCMMMMMMCCMMCCMMMMMM'
]
level_map2 = [
' ',
' ',
' E ',
' CCC CCC CC CCC CCC CC ',
' P C E C ',
' CC CCCC EMC CCCC ',
' ECC CC C ECC CC C',
' CCMM CCC M CCMM CCCE M',
' MMMM E MC M MMMM MC M',
' CCCC MMMMCC CC MMCCCM CCCC EMMMMCC CC MMCCCM',
'CCCCMMMMCCMMMMMMCCMMCCMMMMMMCCCCMMMMCCMMMMMMCCMMCCMMMMMM'
]
# the base path of graphic resources of the game
base_path = path.join(path.dirname(__file__), '..', 'graphics')
# the graphics of different types of tiles
tile_types = {
'T': path.join(base_path, 'Tiles', 'grassMid.png'),
'X': path.join(base_path, 'Tiles', 'grassCenter.png'),
'C': path.join(base_path, 'Tiles', 'castleMid.png'),
'M': path.join(base_path, 'Tiles', 'castleCenter.png'),
}
# default settings
tile_size = 64
clock_tick = 1000
screen_width = 1200
screen_height = len(level_map) * tile_sizeIn door.py, the door class represents a door within the game, handling its creation, visual representation, and updating its position
import pygame
from settings import base_path
from os import path
class Door(pygame.sprite.Sprite):
def __init__(self, pos):
super().__init__()
sprite = pygame.image.load(path.join(base_path, 'Tiles','window.png'))
sprite_surface = pygame.transform.smoothscale(sprite, (32, 64)).convert_alpha()
sprite_surface.set_colorkey((0, 0, 0))
self.image = sprite_surface
self.image.blit(sprite_surface, (0, 0))
self.rect = self.image.get_rect(topleft=pos)
def update(self, x_shift):
self.rect.x += x_shiftOnce you've created the door.py class, we would need to import door into level.py and add it into the setup_level method in level.py to make sure that it appears properly on the level.
from door import Door
class Level:
# Other Methods
def setup_level(self, layout):
self.tiles = pygame.sprite.Group()
self.player = pygame.sprite.GroupSingle()
self.enemies = pygame.sprite.Group()
self.doors = pygame.sprite.GroupSingle()
for row_index, row in enumerate(layout):
for col_index, cell in enumerate(row):
x = col_index * tile_size
y = row_index * tile_size
if cell == 'P':
player_sprite = Player((x, y))
self.player.add(player_sprite)
elif cell == 'E':
enemy = Enemy((x,y))
self.enemies.add(enemy)
elif cell == 'J':
door_sprite = Door((x, y))
self.doors.add(door_sprite)
elif cell != ' ':
tile_sprite = Tile((x, y), tile_size, cell)
self.tiles.add(tile_sprite)Go to levels.py and add a function to check if the player has collided with the Door, then we can implement a "change_level" function to clear out the current tiles, and set up the new level. This allows the player to change levels when interacting with doors. Don't forget to import the new level.
def check_door_collision(self):
player = self.player.sprite
doors = self.doors.sprites()
for door in doors:
if pygame.sprite.collide_rect(player, door):
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
self.change_level(level_map2) # Change the level when the player interacts with the door
def change_level(self, level=level_map):
# Clear all sprite groups
self.tiles.empty()
self.player.empty()
self.enemies.empty()
self.doors.empty()
# Load the new level data
self.setup_level(level)
def run(self):
#Other Methods
self.check_door_collision()
self.doors.draw(self.display_surface)
self.doors.update(self.world_shift)Essentially a start menu is just a blank screen with buttons on them, so in order to make it let's first create a button class called button.py, and inside we will have properties that will do something once someone clicks on the button.
import pygame
class Button():
def __init__(self,x,y, image):
self.image = image
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.clicked = False
def draw(self, surface):
action = False
# get mouse position
pos = pygame.mouse.get_pos()
# check mouseover and clicked conditions
if self.rect.collidepoint(pos):
if pygame.mouse.get_pressed()[0] == 1:
action = True
self.clicked = True
if pygame.mouse.get_pressed()[0] == 0:
self.clicked = False
# draw button on screen
surface.blit(self.image, self.rect)
return actionIn video games, Menu Screens are usually found before the start of the game, like lets say a Mario game, then are we able to initialize the game. Therefore, we would need to put this function on the main.py and set it up before we are running the level!
import pygame, sys
from settings import *
from tiles import Tile
from level import Level
from button import Button
from os import path
class Main:
# Pygame setup
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((screen_width, screen_height))
self.clock = pygame.time.Clock()
self.level = Level(level_map, self.screen)
self.main_menu = True
self.start_img = pygame.image.load(path.join(base_path, 'buttons', 'start-1.png')).convert_alpha()
self.exit_img = pygame.image.load(path.join(base_path, 'buttons', 'exit-1.png')).convert_alpha()
self.menu_img = pygame.image.load(path.join(base_path, 'title.png')).convert_alpha()
self.menu_background_img = pygame.transform.scale(pygame.image.load(path.join(base_path, 'sky1.png')).convert(), self.screen.get_size())
self.game_background_img = pygame.transform.scale(pygame.image.load(path.join(base_path, 'bg.png')).convert(), self.screen.get_size())
self.start_button = Button(screen_width//2 - self.start_img.get_width()//2, 500, self.start_img)
self.exit_button = Button(screen_width//2 - self.exit_img.get_width()//2, 600, self.exit_img)
pygame.display.set_caption("2D Platformer Game")
def run(self):
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
self.screen.fill((0, 0, 0)) # Clear the screen with black before drawing anything
# Check if the game is in the main menu state
if self.main_menu:
# Load the main menu background image
background_img = self.menu_background_img
else:
# Resize the background image to match the screen size
background_img = self.game_background_img
# Blit the background image onto the screen
self.screen.blit(background_img, (0, 0))
# Handle game logic based on the current state
if not self.main_menu:
self.level.run()
if not self.level.check_player():
self.main_menu = True
self.level.change_level(level_map)
if self.main_menu:
# Draw the main menu buttons
self.screen.blit(self.menu_img, (screen_width//2 - self.menu_img.get_width()//2, 50))
self.start_button.draw(self.screen)
self.exit_button.draw(self.screen)
if self.start_button.clicked:
self.main_menu = False
if self.exit_button.clicked:
return 2
else:
self.level.run()
pygame.display.update()
self.clock.tick(clock_tick)Go back to level.py and add a check_player() method, this will check if either the player has depleted its health, or fall into oblivion.
def check_player(self):
if self.player.sprite.current_health == 0 or (self.player.sprite.rect.left < 0 or self.player.sprite.rect.right > screen_width) or (self.player.sprite.rect.bottom > screen_height or self.player.sprite.rect.bottom < -64):
return 0
return 1Return to main.py and to make a Game Over screen, we need a way of overseeing when the player dies. Once they died, we will notify the main to reset the level. The "if name=='main'" method is used at the end to check and see if there is a return value attached, then we will create a new instance (to load the resources again) therefore resetting the level. If the exit button is clicked, we will receive a "2" which will fulfill the condition and end the loop, terminating the game.
def run(self):
#Other attributes
if not self.level.check_player():
self.main_menu = True
self.level.change_level(level_map)
if self.exit_button.clicked:
return 2
if __name__ == '__main__':
main = Main()
out = 0
while out != 2:
out = main.run()