Credit, Where Credits Due

Cinema screen with movie credits

As a brief aside, after building a (mostly) non-functioning menu state, I have taken the time to build a quick state for showing a scrolling credits page as proof of concept for switching between states in the state manager. This allows for some exploration of some other techniques related to how we display the screen, as well as proving that we can switch seamlessly from one state to another. So, without further ado, let’s dive right in.

The new Credits state is built in a new .py file named CreditsState.py, and like the MainMenu State, it inherits functionality from the base State object, so some of the heavy lifting relating to drawing canvas setup is already taken care of for us.

In the __init__ method, we do define some extra instance variables which we’ll use later:

    def __init__(self, game):
        """Perform any on-load initialisation of the state.  e.g. preload
        resources, set persistent buffer lengths, etc. """

        # Call the parent class init method so we can make use of any generic
        # setup we can use.
        super().__init__(game)

        # Do our custom initialisation here
        self.finished = False
        self.scroll_offset = self.game.height
        self.scroll_speed = self.game.height / 10

        self.credits_surf = None    # Placeholder for the rendered credits

The finished entry is used to terminate this state when we are done. The scroll_offset is used to locate the top of the scrolling credits canvas, and the scroll speed sets the rate at which we want to move the credits by. In this case, dividing the screen height by 10 says that we want to scroll a screen’s worth of data in ten seconds. We also define an empty instance variable here to which we will attach a new surface in the startup method when the state is instantiated.

The startup method does most of the work for us here. Since the credits are static, we don’t really want the overhead of recomputing them every frame, so instead, we pre-render them when the state becomes active and store this on a separate canvas object. This canvas can then simply be blitted at the correct position onto the main screen canvas each frame, which is a much quicker operation.

So, we start by calling the superclass setup function so it can do its work, then we define a list of tuples which contains the credits themselves. Each tuple consists of a style element which will determine the size of the text we want to render, the text itself and another tuple containing the RGB colour values for the text. Spacing is provided by simply rendering an empty string.

The style tags themselves are stored in a Python dictionary where each tag is linked to a tuple containing the text height in pixels along with a boolean flag which will eventually be used to render fonts bold or normal (although this will have to wait for a future revision of the code)

We then determine how high our drawing surface will need to be for rendering all the credits text and generate a surface of the appropriate height. The extraction of the tags from the main credits list to do this is performed using a list comprehension, allowing us to extract simply the tags from the credits list and process those in isolation.

We then loop over the list of credits and render each string at the appropriate vertical offset from the top of the canvas using the draw_text function we looked at when building the main menu.

This is by far the longest and most complex piece of code within this class.

    def startup(self):
        """Perform any state specific initialisation each time the state
        becomes current (e.g. resetting scores, setting player positions,
        etc."""
        super().startup()

        # We'll generate the credits here since we want to render them to a
        # surface which we'll simply use throughout the life of the object.
        # That way, we can be more efficient.

        # The credits that we want to scroll through.  Consists of a list of
        # tuples which defines: Style (based on HTML markup tags), the Text
        # to display (line by line), the colour of the text (as an RGB tuple)
        # Where we want to add spacing, we simply add blank lines of the
        # appropriate style
        credits = [
            ("h1", "PONG", (255, 255, 161)),
            ("h2", "", (0, 0, 0)),
            ("h2", "", (0, 0, 0)),
            ("h2", "Original Concept", (255, 255, 255)),
            ("p", "Allan Alcorn", (200, 200, 200)),
            ("p", "Ted Dabney", (200, 200, 200)),
            ("p", "Nolan Bushnell", (200, 200, 200)),
            ("h1", "", (0, 0, 0)),
            ("h2", "This Implementation", (255, 255, 255)),
            ("p", "Mark Edwards", (200, 200, 200)),
            ("h1", "", (0, 0, 0)),
            ("h1", "", (0, 0, 0)),
            ("p", "Press the 'Escape' key to return to the menu", (128, 128,
                                                                   128))
        ]

        # Definitions for the various styles in the credits.  Typically
        # related to font height, but also to whether fonts are set bold or not
        styles = {
            "h1": (200, True),
            "h2": (25, True),
            "p": (20, False)
        }

        canvas_height = 0

        # Extract just the style tags from the credits list
        credit_styles = [s[0] for s in credits]

        for i in credit_styles:
            canvas_height += styles[i][0] + 5   # +5 adds a 5px buffer around
                                                # lines

        self.credits_surf = pygame.Surface((self.display_width, canvas_height))
        self.credits_surf.fill(self.black)

        # Get half the first offset for our starting point
        v_offset = 0

        for cstyle, text, colour in credits:
            draw_text(self.credits_surf,
                      text,
                      pygame.font.get_default_font(),
                      styles[cstyle][0],
                      self.display_width // 2,
                      v_offset + styles[cstyle][0] // 2,
                      colour)

            v_offset += styles[cstyle][0] + 5

Our handle_events method is pretty simple. We check for the existence of a quit event and set our exit flag if one occurs – although this shouldn’t happen during this state. The only user input accepted here is the escape key which we trap in much the same way we did for inputs in the Main menu

    def handle_events(self):
        """Run the 'event pump' and handle any events that arise.  This base
        class provides a simple event pump that merely checks the presence of
        the pressing of the space bar and raises the pygame.QUIT event.
        Processing of the pygame.QUIT event causes the game itself to
        terminate"""
        # Run event pump
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game.is_running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.finished = True

During the update, we first check if the state is finished (ie: the user pressed the escape key) and if so, immediately pop the state off the stack which should return us to the menu.

If we haven’t exited, then we move the top of the credits upwards by a fraction of the scroll speed we defined determined by our frame rate. This way, we achieve the illusion of a smooth scroll. We also check to see if our scrolling canvas has completely run off the top of the display. If it has, we simply move it back to the bottom to begin scrolling up again.

    def update(self, game_time, dt):
        """Update the simulation"""
        # The only interaction here is if the user has pressed the escape
        # key, in which case, we simply pop this state off of the stack...
        if self.finished is True:
            self.game.state_manager.pop()

        # Move the credits up the screen.
        self.scroll_offset -= self.scroll_speed * dt

        # If we've scrolled off the top of the screen, wrap around again...
        if self.scroll_offset < -self.credits_surf.get_height():
            self.scroll_offset = self.display_height

Finally, the display method consists of a mere three lines. It fills our general draw surface with black, then overprints that with our credits text surface we pre-rendered at the correct position, then blits the display surface onto the main game window.

    def display(self):
        """Draw the current frame"""
        self.display_surface.fill(self.black)

        self.display_surface.blit(self.credits_surf, (0, self.scroll_offset))

        self.game.display_window.blit(self.display_surface, (0, 0))

And that’s it for this state. Before we can use it though, there are a couple of edits we need to make to previous code.

Firstly, we need to make sure that the state is added to the ‘states’ list in the main script so it can be loaded:

    states = [
        {"name": "MainMenu", "file": "MainMenuState", "default": True},
        {"name": "Credits", "file": "CreditsState", "default": False},
    ]

Then in the MainMenuState.py file we need to update the appropriate lambda function to call

self.game.state_manager.push("Credits")

Astute readers among you will have noticed that I’ve also amended the StateManager push method slightly. Previously, it required that we push the state object itself into the method, however to present a friendlier interface, I’ve amended it now so that a state can simply be invoked by name.

As usual, the updated code is in the GitHub repository for the curious.

Failure to accord credit to anyone for what he may have done is a great weakness in any man

William Howard Taft

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.