Floating Info Text Lab

../_images/floating-text.gif

Floating info-text

Goals

  • Use encapsulation and inheritance in a Pygame program.

  • Use static variables to store objects inside a class.

  • Use factory (class) methods to create objects for us.

  • Tune the API to make using the code extremely simple.

Starter code

What you need to do

In the previous exercise, we created a Box class and a LockableBox class. Both raised errors when the box was full or locked. The Pygame program in main.py simply printed out the error, which isn’t terribly professional.

Let’s create the ability to add floating info text to our Pygame. We will display info text to show we have added an item as well as when there is an error. We will have the positive messages do something different than the negative mesasges.

  1. Create a new file called info_text.py.

  2. Create the class InfoText in that file with the attributes shown in the text UML below (py attention to the UML indicating private/public attributes). For typing the box attribute, import the Box class from box.py. Look in main.py to see how the import was done in that file.

    ../_images/InfoTextUML.svg

    Complete the __init__ method. It’s not important to know how everything works just yet. Stub in the methods by just putting pass in them. I will explain them as we go along.

  3. In main.py, create a global variable called info and set it to None. Also, at the top, put from info_text import InfoText. Just like what is there for importing Box.

  4. Where we add a new item to a Box object, create a new InfoText object.

    info = InfoText(box, "Item added")
    
  5. Where you draw the boxes, also draw the info object, but only if it is not None. Hint: info.draw(screen). Don’t put this in the boxes loop. This is a separate entity. Info text is not a box.

  6. When you click the boxes you won’t see anything because the InfoText.draw() method is empty. Here is some code you can use:

    # AT THE TOP OF THE info_text.py FILE (NOT IN THE CLASS)
    import pygame
    pygame.font.init()
    FONT = pygame.font.SysFont("Arial", 15)
    
    # the body of the InfoText.draw method
    text = FONT.render(self._message, True, self._color)
    x = self._box.x
    y_offset = self._lifespan - self._frames_remaining
    y = self._box.y - 15 - (y_offset)
    screen.blit(text, (x, y))
    
    self._frames_remaining -= 1
    

Run the code and ensure you are seeing "Item added" rising over the box you clicked.

  1. We want the text to go away after 30 frames, yet, it currently persists. Fill in the InfoText.is_alive() method. The message is considered “alive” when it has frames remaining. A value of 0 frames remaining would mean it is “dead” and needs to be removed.

  2. In main.py after we draw the info text, use the is_alive method. If it is not alive, then set info = None. This will prevent it from drawing (and will destroy the message).

Run the code and ensure that the message dissappears before reaching the top of the window. This should happen after one second.

  1. You also might notice that you can only every have one info text active at a time. This is slightly annoying. In main.py make a list of info texts. When an info text is created, add it to the list.

  2. Use a loop to draw them all.

  3. When an info text is no longer “alive”, have your program remove it from the list.

Run the program. You should see multiple floating texts when the box is clicked multiple times.

API Issues

This seems to work quite well, except I don’t like how much main.py has to manually manage my InfoText objects. main.py needs to create a list, add to that list, loop through the list to draw each object, and then manually remove them when they expire. Wouldn’t it be nicer if all we had to do is create the objects and have the class itself manage them all?

You could argue to leave the class as it is, so we can provide users the option to manually manage the info text objects. That is a good idea and perhaps we could come up with a InfoTextManager class if people didn’t want to manage the objects. For the purpose of this exercise, we will just continue to modify the InfoText class to do the management.

  1. In the InfoText class, we are going to create a class variable. A list of every info text object that will be created. Right before the __init__ method, put the class variable.

all_info_texts: List['InfoText'] = []
  1. Next we will create a class method, InfoText.create(). This will be responsible both for creating new InfoText objects and adding them to the InfoText.all_info_texts list. Note: this is a method called off of the class and not any particular object. I’ll show how to call the method in the next couple of steps.

@classmethod
def create(cls, box: Box, message: str) -> 'InfoText':
    info_text = cls(box, message)
    cls.all_info_texts.append(info_text)
    return info_text
  1. In main.py, we need to do some clean-up. Get rid of: - The info_texts list. - The loop drawing (and removing from) the info_texts list. - The creation of InfoText objects.

If you run the program, you should see no texts when you add an item.

  1. When you add an item in main.py, call the class method we made in question 13. This is done by calling the method off of the class itself, not an object. Exactly like: InfoText.create(box, "Item added"). Remember, this creates a new InfoText object for you and automatically adds it to a list of all text objects inside of the InfoText class itself.

  2. If you want to see something when you add an item, you can go to the InfoText.create class method and print out the message when it is created. Remove this later, though, this is only for debugging purposes.

  3. Create another class method InfoText.draw_all(cls, screen: pygame.Surface): -> None. This will loop through the InfoText.all_info_texts list and call their respective draw(screen) methods.

  4. Where you draw the boxes in main.py call InfoText.draw_all(screen) to draw all the texts in the class’s list.

Run the program and add items to the boxes, you should see texts floating up. You will notice they don’t dissappear.

  1. Modify the InfoText.draw_all class method to also remove them from the list when they are no longer “alive”. Use each object’s .is_alive method to help determine this.

Now let’s create some error text. We want error text to be essentially the same, but, we want it to look and behave differently. Maybe it should be red and shake instead of floating up.

  1. Create an ErrorText class in info_text.py. Inherit from InfoText. Just write pass for its contents. Import it into main.py where we imported InfoText.

  2. In main.py where we print out the error messages for box is full or locked, remove the print statements and use ErrorText.create(box, message) to create the error messages. Yes, even class methods are inherited. Initially, ErrorText will have all the functionality and capabilities as InfoText.

  3. Just change the error text to "Locked!" and "Full!". We don’t want to clutter the UI.

At this point, you should also see the “error” text floating above the boxes. This is cool, but, we do want to make the text look and behave differently.

  1. Define an __init__ method for ErrorText. Be sure to call super().__init__() and pass it all the relevant arguments so it can set up all the attributes for us. Do this and ensure everything works the same as before.

  2. After ErrorText.__init__ calls super’s init, we want to override the _color attribute to (200, 0, 0).

At this point, when you cause an error with one of the boxes, it should float up text like normal, but this time it will be red. Make sure adding an item is green (regular info text) and full/locked (error) text is red.

Now we need to change the way the error text behaves. You could leave it floating, but it’s cooler to have error text shake.

  1. At this point we want to override the draw method in ErrorText. The problem is, I’m noticing the original draw method is doing things that I want inherited (drawing text of a particular color and decreasing the frames remaining) and things I don’t (the animation of floating upwards). Normally, one would factor out the animation portion, inherit the drawing, and then override the animation, but, that would get confusing in this context. Just copy and paste my revised method for ``ErrorText.draw``. You will need to import math at the top of the info_text.py file.

    x_offset = self._frames_remaining / 2 * math.sin(self._frames_remaining)
    x = self._box.x + x_offset
    y = self._box.y - 20
    
    text = FONT.render(self._message, True, self._color)
    screen.blit(text, (x, y))
    
    self._frames_remaining -= 1
    

At this point, you should have green add-item text that floats up and red error text that shakes. The overlooked bug is that if you have multiple error texts, they overlap. Oh well. Fixing that is outside the scope of this exercise. One could also refactor the draw methods and utilize proper inheritance.

The API

Let’s review what kind of API we just created.

# main.py

from info_text import InfoText, ErrorText

...

InfoText.create(box, "added item")
ErrorText.create(box, "Full!")

...

InfoText.draw_all(screen)

Hardly any points of contact and we have a fully functional, fully managed info-text system for pygame.

Hope you enjoyed this walk-through.

My Solution

Here is my solution.