Skip to main content

Command Palette

Search for a command to run...

Building a fantasy top-down game in Python (Part 4)

Published
7 min read
Building a fantasy top-down game in Python (Part 4)
F

An aspiring developer

To recap, we now have

  • A map layout with hazard, enemy, player and coin components

  • Character animations

  • A player that can move and attack

  • Enemies that can chase and attack the player

At this point, we could integrate more complex pathfinding algorithms such as A* or Dijkstra’s for the enemies, but in a game as small as this one I just didn’t see a point in doing that (a.k.a I’m very lazy and simply did not want to). But to wrap it up, we need to make some finishing touches. A good game needs a start screen, a help manual, and an ending based on success or failure.

Let’s start with the ‘start’ screen. First, we need to keep track of what part of the game the player is in: Have they started? Did they finish? Are they stuck?
When you have a problem like this, where you need to keep track of a state across multiple files, its best to create a variable in settings.py that we can import everywhere. In this case, I made 4 variables : whichever one of them is true, gets displayed. If none are true, it means the game has started. Now you might wonder, would it not have been easier to make one variable and set it to either ‘intro’, ‘help’, ‘success’ or ‘failure’? The answer would be yes, you absolutely can, I just didn’t think of that when I made this and I don’t care enough to optimize it now.

in_intro = True #we start in the intro
in_help = False
success = False
failure = False

One thing to note is that when you import a data structure from a different file, your local file creates a copy of it and edits the copy instead of the original. If we keep editing the local copies, our changes wouldn’t be reflected throughout the project. To fix this, instead of using from settings import *, we say import settings and then use settings.variable_name , which is the original variable, throughout our project.

We can then write a function to display the intro screen as such

def intro_screen(self, clicked):
        bg = transform.scale(image.load('assets//screens//bg.jpg'), (1000,500)) # did not design the bg
        banner = image.load('assets//screens//banner.png').convert_alpha() #i designed the banner, fun fact, I styled it exactly like the actual Geronimo stilton book title
        button = transform.scale(image.load('assets//screens//button.png'), (300, 50))
        self.screen.blit(bg, (0,0))
        self.screen.blit(banner, (0,50))
        self.screen.blit(button, (400,250))
        col_0 = "#312400"
        col_1 = "#312400"
        col_2 = "#312400"
        game_font = font.Font("assets\\screens\\Pixellari.ttf", 30)
        sb_rect = Rect(520,260, 150, 50)
        self.screen.blit(button, (400,310))
        h_rect = Rect(520,320, 150, 50)
        self.screen.blit(button, (400,370))
        q_rect = Rect(520,380, 150, 50)
        buttons = [q_rect, sb_rect, h_rect]
        l,m,r = mouse.get_pressed()
        pos = mouse.get_pos()
        if clicked: 
            for i, button in enumerate(buttons): 
                if button.collidepoint(pos):
                    if i == 0:
                        col_0 = "#966825"
                        return 'quit'
                    elif i == 1:
                        col_1 = "#966825"
                        return 'start'
                    else: 
                        col_2 = "#966825"
                        return "help"
        quit_button = game_font.render("Quit", True, col_0)
        start_button = game_font.render("Start", True, col_1)
        help_button = game_font.render("Help", True, col_2)
        self.screen.blit(start_button, sb_rect)
        self.screen.blit(help_button, h_rect)
        self.screen.blit(quit_button, q_rect)
        return None

I will link all the assets used here below, but basically what this does is creates a screen with 3 buttons (rects that do something when clicked) - start, help and quit, and return the same depending on which one is clicked.

The help screen is similar, except the only button it has is a back button which goes back to either the intro screen, or the game depending on whether or not the player has started.

def show_help_screen(self,clicked):
        self.screen.fill('white')
        help = transform.scale(image.load('files\\assets//screens//help screen.png'), (1000,500))
        self.screen.blit(help, (0,10))
        back = transform.scale(image.load('files\\assets//screens//back.png'), (100,60))
        back_rect = self.screen.blit(back, (450,0))
        back.get_rect()
        pos = mouse.get_pos()
        if clicked:
            if back_rect.collidepoint(pos):
                return 'back'
        return

Lastly, the success/failure screens will be the same except for their backgrounds (I put the text on the backgrounds itself so I wouldn’t have to render text in pygame).

def success_failure_screen(self, clicked, img):
        bg = transform.scale(image.load(img), (1000,500)) # did not design the bg
        button = transform.scale(image.load('files\\assets//screens//button.png'), (300, 50))
        self.screen.blit(bg, (0,0))
        self.screen.blit(button, (370,250))
        col_0 = "#312400"
        col_1 = "#312400"
        col_2 = "#312400"
        game_font = font.Font("files\\assets\\screens\\Pixellari.ttf", 30)
        sb_rect = Rect(470,260, 150, 50)
        self.screen.blit(button, (370,310))
        h_rect = Rect(490,320, 150, 50)
        self.screen.blit(button, (370,370))
        q_rect = Rect(490,380, 150, 50)
        buttons = [q_rect, sb_rect, h_rect]
        l,m,r = mouse.get_pressed()
        pos = mouse.get_pos()
        if clicked: 
            for i, button in enumerate(buttons): 
                if button.collidepoint(pos):
                    if i == 0:
                        col_0 = "#966825"
                        return 'quit'
                    elif i == 1:
                        col_1 = "#966825"
                        return 'start'
                    else: 
                        col_2 = "#966825"
                        return "help"
        quit_button = game_font.render("Quit", True, col_0)
        start_button = game_font.render("Restart", True, col_1)
        help_button = game_font.render("Help", True, col_2)
        self.screen.blit(start_button, sb_rect)
        self.screen.blit(help_button, h_rect)
        self.screen.blit(quit_button, q_rect)
        return None

These functions don’t actually DO anything just yet because, well, we haven’t called them yet! In our main file’s run function, we need to add a couple of conditionals to decide which screen to render. But before everything, we need to make another variable called ‘clicked’ in settings.py and import it the same way we imported the other four variables. What this variable does is detects whenever the player clicks on the screen by turning True or False. Now in our main file’s run function, we can set settings.clicked = False so that it resets each time. Then, in the event loop, we check for the event of MOUSEBUTTONDOWN and set settings.clicked = True. We can then create a chain of conditionals :


if self.in_intro:
   if not mixer.music.get_busy():
      mixer.music.play(-1) #optional, starting music that i loaded at the beginning
   action = self.intro_screen(files.settings.clicked) #stores start, help or quit based on what self.intro_screen() returns
   if action == "start":
      self.in_intro = False
      mixer.music.stop()
   elif action == "quit":
      quit()
      sys.exit()
   elif action == "help":
      self.in_intro = False
      in_help = True
      mixer.music.stop()

The conditionals for help and success/failure will be sort of similar :

          elif in_help:
                action = self.show_help_screen(files.settings.clicked)
                if action == 'back':
                    in_help = False
                    if self.was_playing:
                        self.was_playing = False  # reset it
                        # resume level
                        continue  # next loop will rerun whichever screen it was on 
                    else:
                        self.in_intro = True
            else:
                if files.settings.success: # im using files.settings.success instead of success because i dont want to check the local copy, i want to check the global copy in the actual file
                    action = self.success_failure_screen(files.settings.clicked, 'files\\assets//screens//Success screen.png')
                    triumph = mixer.Sound('files\\assets\\sounds\\11l-victory_trumpet-1749704469779-358762.mp3')
                    if self.sound_played == False:
                        triumph.play(1)
                        self.sound_played = True

                    if action == "start":
                        files.settings.success = False
                        self.level = Level(self.screen)
                        self.sound_played = False
                        self.in_intro = False

                    elif action == "quit":
                        quit()
                        sys.exit()

                    elif action == "help":
                        self.in_intro = False
                        self.was_playing = True
                        in_help = True

                elif files.settings.failure:
                    action = self.success_failure_screen(files.settings.clicked, 'files\\assets//screens//Failure screen.png')
                    failure = mixer.Sound('files\\assets\\sounds\\losing-horn-313723.mp3')
                    if self.sound_played == False:
                        failure.play(1)
                        self.sound_played = True
                    if action == "start":
                        files.settings.failure = False
                        self.level = Level(self.screen)
                        self.sound_played = False
                        self.in_intro = False
                    elif action == "quit":
                        quit()
                        sys.exit()
                    elif action == "help":
                        self.in_intro = False
                        in_help = True
                        self.was_playing = True
                else:
                    check_for_help = self.level.run() #tweak the run function in level to return help if we clicked help there
                    if check_for_help == 'help':
                        self.was_playing = True
                        in_help = True

For this section, I made another class variable called was_playing to check if the game was already in session when the player clicked on the help manual (they might need help during the game) or whether they were on the intro screen. Based off of this, we choose which screen to re-render. Now, another problem I ran into was when rendering the success/failure screen, to be more specific, restarting the game once the player finished. I realized that simply setting success/failure = False did not start a NEW game. To do that, we create a new Level object instead which creates a new game afresh.

One problem in this game is that we don’t really let the player know how they’re doing, as in the player is unaware of how much health, points, coins etc. they have. We can add a small section on top of the screen during the game to display these stats since we’re already keeping track of them in our game. To do this, I made another class (the last one I swear) called UI.

class UI:
    def __init__(self):
        self.screen = display.get_surface()
        self.font = font.Font('files//assets//screens//Pixellari.ttf', 20)
        self.health_bar = Rect(115,10, h_width, h_height)
        self.h_col = "#F3E73F"


    def display(self, player):
        dark_overlay = Surface((350,220), SRCALPHA)
        dark_overlay.fill((0,0,0,100))
        self.screen.blit(dark_overlay, (0,0))

Right now, display only creates the transparent surface to put the stats on, but I added a bunch of code to create the health bar, display the score, current spells, magical objects collected etc.

Conclusion

https://github.com/fa22991/Python-top-down-game.git

Above is the link to the repo (for some reason the only way I could upload it was as a zip file).
Thank you for following along till the end!

Building a fantasy top-down game in Python (Part 4)