2
\$\begingroup\$

I've made significant progress porting my game from print-output to Pygame over the last few days, and I think I'm almost there!

Unfortunately, I'm beginning to suspect one of the methods I'm using to display my output is deeply flawed when it comes to what I eventually want it to do.

My goal is to create a "Game log" that displays the output from the most recent actions the player has taken.

For example, if the player entered a cabin, searched the cabin, then picked up the key they found there, I would like the gamelog to display

You enter the cabin

You find a Rusty Key

You take the Rusty Key

Currently, any time the gamelog updates, it replaces the previous output. This poses several issues:

1.) In a game of this style, a player can easily forget what the last few things they've done are. Not being able to see recent actions can be tedious and annoying to the player.

2.) The method I use to allow players to check their inventory works through a for-loop. With the way the gamelog works right now, only the most recently picked up item will display.

3.) In some situations, the info I need to display in this output is contained in a set. This system for displaying output can not take sets as an argument.

The code below is what I'm using to generate the output. This comes from this set of functions called "Pygame_Functions"

https://github.com/StevePaget/Pygame_Functions

which has been quite helpful.

I'm beginning to think that this may not be the best way to achieve what I'm trying to do. It seems like this is only really effective for rendering a singular message.

class newLabel(pygame.sprite.Sprite):
    def __init__(self, text, fontSize, font, fontColour, xpos, ypos, background):
        pygame.sprite.Sprite.__init__(self)
        self.text = text
        self.fontColour = parseColour(fontColour)
        self.fontFace = pygame.font.match_font(font)
        self.fontSize = fontSize
        self.background = background
        self.font = pygame.font.Font(self.fontFace, self.fontSize)
        self.renderText()
        self.rect.topleft = [xpos, ypos]

    def update(self, newText, fontColour, background):
        self.text = newText
        if fontColour:
            self.fontColour = parseColour(fontColour)
        if background:
            self.background = parseColour(background)

        oldTopLeft = self.rect.topleft
        self.renderText()
        self.rect.topleft = oldTopLeft
        updateDisplay()

    def renderText(self):
        lineSurfaces = []
        textLines = self.text.split("<br>")
        maxWidth = 0
        maxHeight = 0
        for line in textLines:
            lineSurfaces.append(self.font.render(line, True, self.fontColour))
            thisRect = lineSurfaces[-1].get_rect()
            if thisRect.width > maxWidth:
                maxWidth = thisRect.width
            if thisRect.height > maxHeight:
                maxHeight = thisRect.height
        self.image = pygame.Surface((maxWidth, (self.fontSize+1)*len(textLines)+5), pygame.SRCALPHA, 32)
        self.image.convert_alpha()
        if self.background != "clear":
            self.image.fill(parseColour(self.background))
        linePos = 0
        for lineSurface in lineSurfaces:
            self.image.blit(lineSurface,[0,linePos])
            linePos+=self.fontSize+1
        self.rect = self.image.get_rect()

And this bit here is the function I'm using to update the output after each player action.

def changeLabel(textObject, newText, fontColour=None, background=None):
    textObject.update(newText, fontColour, background)

My general concept for how to get my gamelog made is that each of these output statements needs to be inserted into the first spot of a list.

Then, display the whole list, and pop off items from the list once they reach a set limit.

If anything on this needs to be clarified, or if other parts of my code are required to understand what my issue is, I'll update this post with whatever is requested.

\$\endgroup\$
2
  • \$\begingroup\$ Have you tried using a Stack containing the Game Log? \$\endgroup\$ May 23, 2018 at 21:03
  • \$\begingroup\$ @SamuelLopes I really don't even know how to begin about doing this. Part of the trouble though is that when rendering my text, I use .split, which won't work with any sets. \$\endgroup\$ May 23, 2018 at 21:16

2 Answers 2

2
\$\begingroup\$

So there's a couple steps to this:

  1. You need a list to handle the log messages.
  2. You need a function to print events to the list. How you do this is up to you, but it needs to be a function you can call whenever these events happen. Something as simple as a custom print function works just fine. Here's a quick idea of how to make the log itself:

    list = []

    def console_print(message): list.append(message)

    console_print("Test")

Then it gets harder because you need to display it as part of the UI. So you need to pick your dimensions and measure it out and determine how many lines of whatever sized font you are going to use. If it is turn based this is all a little easier to do because you can just update it every turn. I personally prefer to blit UI elements separately with pygame, so my order of display would be something like this for each turn:

  1. Create a list of n messages where n is the number of lines you can fit in your display element.
  2. Take the last n elements from your log list and append them to the new list.
  3. Create a surface the size of your UI element.
  4. For n lines that you want to display, use font.render to add a line. You'll need to calculate the X and Y coordinates for each one which is a little trial and error.
  5. Now that you have this surface and you've rendered lines of font onto it, you can blit it back to your main surface as part of your update() process. You would ideally do this each turn, and not just for your log but for every UI element.

Putting UIs together with pygame is my favorite part of it. Anything is possible once you start getting your head wrapped around blitting surfaces on top of surfaces. Custom-design your update() method for your game and your UI! I'll dig up the last console I made next time I'm at my PC so you can see some real code. I'm no expert but I can help you here. Good luck!

Edit: I'm having a very hard time getting my little python snippet to indent properly on the Stack Exchange app. Sorry.

Edit #2: Alright I got home and whipped up a little example. I'm not the best at being "pythonic" but here you can hopefully see how I create a surface for the log, populate it with rendered text, and THEN blit it onto the main surface (which for this example is just a black space). Dividing your pygame UI up into HUD sections is a big part of jumping from making text-based programs to working with pygame's surfaces. Please forgive my use of magic numbers.

# A little pygame console
# press "t" to add messages to the continuously updatding HUD element


import pygame

pygame.init()


# constants
RES = (800, 600)
FPS = 30

MAIN_SURFACE = pygame.display.set_mode(RES)
CLOCK = pygame.time.Clock()

pygame.display.set_caption("Example Console UI Element")

the_font = pygame.font.Font(None, 32)

message_log = []

def console_print(message):
    message_log.append(message)

def update_log():
    # create a surface for the log:
    # This one the same width as the RESOLUTION and 1/3 the height
    log_surf = pygame.Surface((RES[0], RES[1] // 3))
    log_surf.fill((80, 80, 80))
    # Populate it with, say, the last three messages:
    # Note: You could do this in a more elegant loop if you wanted to. 
    #       You would probably be served by checking to see if you have any messages at all before
    #       searching through the message log, to avoid looking for elements that aren't in the list. 
    #       for this example I added 3 placeholder messages at the start of the main function
    #       to avoid that problem here.
    new_log = []
    m1 = message_log[-1]
    m2 = message_log[-2]
    m3 = message_log[-3]
    new_log.append(m1)
    new_log.append(m2)
    new_log.append(m3)
    font_y = 0
    for m in new_log:
        message = the_font.render(m, False, (255, 255, 255))
        log_surf.blit(message, (0, font_y))
        font_y += 34  # gives a little padding for the next message
    # blit it to the main surface in a spot where it'll fit snugly:
    # sorry for the magic numbers, ideally you would pre-define these positions
    # as variables
    MAIN_SURFACE.blit(log_surf, (0, 400))


def update():
    MAIN_SURFACE.fill((0, 0, 0))
    update_log()
    pygame.display.flip()

def main():

    console_print("Placeholder message 1")
    console_print("Placholder message 2")
    console_print("Placeholder message 3")

    messages = 0

    running = True
    while running:
        for event in pygame.event.get():
            if event.type is pygame.QUIT:
                running = False
            if event.type is pygame.KEYDOWN:
                if event.key == pygame.K_t:
                    # advance the turn as an example
                    messages += 1
                    console_print("Message # " + str(messages))
                    # test in the terminal
                    print("Message # " + str(messages))


        update()

        CLOCK.tick(FPS)


    pygame.quit()

if __name__ == "__main__":
    main()

There you go! By messing around with the numbers here you can hopefully get a feel for messing with surface dimensions. I apologize in advance if this is stylistically bad but it's worked well for me in many projects. Pygame has built-in sub-surface capabilities but I have not deeply explored those at all. Hard-coding your dimensions is not a bad way to start, I feel. I hope that helps and good luck!

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Hey! I ended up using basically this method. Didn't realize I never self-answered or accepted any other response. Thanks for the input! \$\endgroup\$ Dec 21, 2018 at 15:42
1
\$\begingroup\$

You can easily shorten the code and find a better setup by putting the last x actions in a list y indices long. Every time there is a new action, the system will shift them closer to one end of the list and cut out the oldest one. The method is quite simple:

make a loop that starts at the last indice:

i = len(your_list_here) #Replace your_list_here with the text list of previous actions
while i > 0: #need to do a loop for all indicies
    i -= 1 #You have to do this task first, or else the program wacks out
    if i > 0:
        ylh[i] = ylh[i-] #I decided to shorten your_list_here for easier time typing
    else:
        ylh[i] = new_string

so then you would make a new array that updates the text through watever method you use.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .