Floating Info Text Lab
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.
Create a new file called
info_text.py
.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 thebox
attribute, import theBox
class frombox.py
. Look inmain.py
to see how the import was done in that file.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.In
main.py
, create a global variable calledinfo
and set it toNone
. Also, at the top, putfrom info_text import InfoText
. Just like what is there for importingBox
.Where we add a new item to a
Box
object, create a newInfoText
object.info = InfoText(box, "Item added")
Where you draw the boxes, also draw the
info
object, but only if it is notNone
. Hint:info.draw(screen)
. Don’t put this in the boxes loop. This is a separate entity. Info text is not a box.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.
We want the text to go away after
30
frames, yet, it currently persists. Fill in theInfoText.is_alive()
method. The message is considered “alive” when it has frames remaining. A value of0
frames remaining would mean it is “dead” and needs to be removed.In
main.py
after we draw the info text, use theis_alive
method. If it is not alive, then setinfo = 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.
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.Use a loop to draw them all.
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.
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'] = []
Next we will create a class method,
InfoText.create()
. This will be responsible both for creating newInfoText
objects and adding them to theInfoText.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
In
main.py
, we need to do some clean-up. Get rid of: - Theinfo_texts
list. - The loop drawing (and removing from) theinfo_texts
list. - The creation ofInfoText
objects.
If you run the program, you should see no texts when you add an item.
When you add an item in
main.py
, call the class method we made in question13
. 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 newInfoText
object for you and automatically adds it to a list of all text objects inside of theInfoText
class itself.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.Create another class method
InfoText.draw_all(cls, screen: pygame.Surface): -> None
. This will loop through theInfoText.all_info_texts
list and call their respectivedraw(screen)
methods.Where you draw the boxes in
main.py
callInfoText.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.
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.
Create an
ErrorText
class ininfo_text.py
. Inherit fromInfoText
. Just writepass
for its contents. Import it intomain.py
where we importedInfoText
.In
main.py
where we print out the error messages for box is full or locked, remove the print statements and useErrorText.create(box, message)
to create the error messages. Yes, even class methods are inherited. Initially,ErrorText
will have all the functionality and capabilities asInfoText
.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.
Define an
__init__
method forErrorText
. Be sure to callsuper().__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.After
ErrorText.__init__
callssuper
’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.
At this point we want to override the
draw
method inErrorText
. The problem is, I’m noticing the originaldraw
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 toimport math
at the top of theinfo_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.