Python 211 - Bitmaps II
Python Asteroids+Invaders on GitHub
I think the bitmaps are ready to be moved into the Asteroids-Invaders repo. I hope to actually use a couple of them in this session. Success! And an amusing mistake.
The code that makes that picture is long, because it involves a lot of data, but actually fairly simple. It could be simpler and perhaps we’ll work on that, but I think it’s more important to get it moved into the real game and to start displaying some bitmaps. That will help keep the customer satisfied. And the programmer, too, because it’s more fun when the game appears to be getting better.
I’ll spare you most of the details. You can check the repo if you want to see what the bitmap code looks like, or review yesterday’s article, which shows most of it.
I’m just going to create a new class, BitmapMaker, and plunk the methods from the invaders repo into it.
There’s a bunch of code in there that isn’t needed, because in the invaders repo, the whole game notion was right there, so it inits all kinds of things that it shouldn’t. I’ll just remove all that.
That’s quickly done. As coded, if we create a BitmapMaker, the instance we get back will contain all the shapes that are defined, in various member values. Let’s see where we could create that in the invaders situation.
For now, I think we’ll put it in the InvaderFleet, but since there will be several objects, like Player and Shield, that will need bitmaps, we may need to figure out a more general scheme. For now, I’ll put it in InvaderFleet.
I pretty much immediately decide that I don’t like that. I have no use for the bitmaps in InvaderFleet. Maybe InvaderGroup would be better.
Oh. I’m glad I interrupted myself. We should commit the BitmapMaker before we mess something up. Commit: bitmap maker moved into repo.
Now let’s see about the InvaderGroup. What I want to do is to pass each invader her bitmaps, at the time we create her.
At first blush, it’s not clear where we could do that:
class InvaderGroup():
def __init__(self):
self.invaders = ()
self.create_invaders()
self.next_invader = 0
def create_invaders(self):
self.invaders = [Invader(x%11, x//11) for x in range(55)]
The bitmaps they get depend on their row, which is here designated as x//11
, which will run from 0 to 4. The zero row is the bottom row on the display. Let’s create five sets of invader bitmaps. I think the steps here will be interesting so you might want to follow along.
First I create a new table in InvaderGroup, that will have 5 pairs of alien bitmaps, the first kind twice, the second kind twice and the third kind once. One pair per row.
class InvaderGroup():
def __init__(self):
self.invaders = ()
invader_table = self.create_invader_bitmaps()
self.create_invaders()
self.next_invader = 0
def create_invader_bitmaps(self):
maker = BitmapMaker()
aliens = maker.aliens
alien_table = (aliens[0:2], aliens[0:2], aliens[2:4], aliens[2:4], aliens[4:])
return alien_table
Next, we want to pass those down to where we create the invaders. We could create them there, but I decided not to. So we do Change Signature on create_invaders
:
def create_invaders(self, invader_table):
self.invaders = [Invader(x%11, x//11) for x in range(55)]
Because I gave the parameter a default in the Change Signature dialog, PyCharm has also changed the call:
def __init__(self):
self.invaders = ()
invader_table = self.create_invader_bitmaps()
self.create_invaders(invader_table)
self.next_invader = 0
Nice. I do kind of wonder why I called them aliens in the BitmapMaker. We should fix that up for consistency. But right now, what I want to do is pass in the right pair to each invader while I create her. But I have that lovely list comprehension there, and while I could jam it all in there it would be nasty.
Fortunately, if I set my cursor on the left square bracket, PyCharm offers to convert that one-liner to a loop. I accept the offer:
def create_invaders(self, invader_table):
self.invaders = []
for x in range(55):
self.invaders.append(Invader(x % 11, x // 11))
Now I can select the right pair readily and put it in as a third parameter to Invader
. But we’d best get Invader ready first. Over in Invader, Change Signature on the init, which starts like this:
class Invader:
def __init__(self, x, y):
self.relative_position = Vector2(INVADER_SPACING * x, -INVADER_SPACING * y)
self.rect = pygame.Rect(0, 0, 64, 32)
Change signature to expect a third parameter, bitmaps. I give the new parameter the default value maps
. So Invader looks like this:
class Invader:
def __init__(self, x, y, bitmaps):
self.relative_position = Vector2(INVADER_SPACING * x, -INVADER_SPACING * y)
self.rect = pygame.Rect(0, 0, 64, 32)
And in InvaderGroup:
def create_invaders(self, invader_table):
self.invaders = []
for x in range(55):
self.invaders.append(Invader(x % 11, x // 11, maps))
And of course maps is not defined yet. First extract the x // 11
as row. That’s Option-Cmd-V and type the name.
def create_invaders(self, invader_table):
self.invaders = []
for x in range(55):
row = x // 11
self.invaders.append(Invader(x % 11, row, maps))
Let’s extract col
for symmetry.
def create_invaders(self, invader_table):
self.invaders = []
for x in range(55):
row = x // 11
col = x % 11
self.invaders.append(Invader(col, row, maps))
Now lets pass the maps, which are, if I’m not mistaken, which assumes facts not in evidence, will be invader_table[row]
.
def create_invaders(self, invader_table):
self.invaders = []
for x in range(55):
col = x % 11
row = x // 11
maps = invader_table[row]
self.invaders.append(Invader(col, row, maps))
I swapped col and row both because col is first in the call and because it put the creation of the variable and the use adjacent.
Tests are green. I’m not ready to commit, however. No, wait, I am. We’re green and at a point where we’re going to start using the bitmaps. I do not really know that these are the right bitmaps and I don’t have a test for that. I’m going to go ahead but when something goes wrong I’ll write a test, I promise.
Commit: passing invader bitmaps to Invader.__init__
Now let’s see if we can display these maps. I happen to know that there’s a problem, however. In the creation, back in the earlier test, I scaled all the bitmaps by 8, to make them easy to inspect on the screen. They should be scaled at 4. I think I’d like to fix that up. I do that and commit it. Now, back to our regularly scheduled broadcast.
The idea is to make the invader use her first bitmap to display herself. Her display logic now is this:
def draw(self, screen):
if screen:
circle_color = "green" if self.relative_position == Vector2(0, 0) else "red"
pygame.draw.rect(screen, "yellow", self.rect)
pygame.draw.circle(screen, circle_color, self.rect.center, 16)
I see that we have a member, self.rect
. How is that set? Excellent:
class Invader:
def __init__(self, x, y, bitmaps):
self.relative_position = Vector2(INVADER_SPACING * x, -INVADER_SPACING * y)
self.rect = pygame.Rect(0, 0, 64, 32)
That’s the correct size for the invader! I think this might just work.
def draw(self, screen):
if screen:
screen.blit(self.bitmaps[0], self.rect)
And when I run the game, this happens:
Now that pleases me. Commit: invaders all display their first picture.
It’s time for a little break and a little reflection.
Reflection
This went about as well as one could expect, perhaps even better. It wasn’t perfect. I had left the line class Game
in when I pasted in the code, and that made some odd things happen until I noticed.
I had trouble with the slicing syntax, which I haven’t used often, so I had to look it up. Then I reflexively typed [2, 4]
where [2:4]
was what was needed.
The best part was that the invader rectangle was already set up correctly, and that the invader moves by setting her rectangle’s center:
class Invader:
def set_position(self, origin):
self.rect.center = origin + self.relative_position
So that made the display blit work as intended. Quite smooth. I had expected something to go wrong, though I couldn’t imagine what it would be. Instead, imagine my surprise when nothing went wrong!
Can we make them animate? I think we can do so quite easily. Shall we try? OK, let’s. I think the easy thing is just to have each invader have a new field that is zero or one, toggling on each move.
He’s wrong.
That does not work, and I suspect that my little tables don’t have in them what I think they have.
I promised to write a test, and so I will. I get this much:
def test_invader_bitmaps(self):
maps = InvaderGroup.create_invader_bitmaps(None)
assert len(maps) == 5
first = maps[0]
assert len(first) == 2
assert first[0] != first[1]
This tells me that the maps are different. That leads me to conclude that I didn’t do the swapping right. I finally isolate my problem:
def set_position(self, origin):
self.image = 1 if self.image == 0 else 1
self.rect.center = origin + self.relative_position
def draw(self, screen):
if screen:
screen.blit(self.bitmaps[self.image], self.rect)
The final 1 in the ternary should be 0. Otherwise self.image
always 1. I’m too clever for my shirt.
Now things work as advertised:
Commit: invaders animate.
Let’s sum up. We have a really good outcome here, let’s not spoil it.
Summary
Well, I interfered with the canine on that simple toggling thing, which would be embarrassing had I not already made much worse mistakes in front of you. I suppose I could have written
self.image = self.image ^ 1
I bet everyone would have loved that a lot. Points for negative clarity. Or, I could have written it out longhand:
if self.image == 0:
self.image = 1
else:
self.image = 1
I’d probably have had a better chance of getting that right except that I swear this is true, I accidentally got it wrong right there.
if self.image == 0:
self.image = 1
else:
self.image = 0
That’s rather amusing, really.
I had considered reversing the bitmaps list on each step and always displaying [0], but that seemed inefficient and not obvious. Too clever for sure.
A mistake. We make them. We fix them. We think what we could have done better. Sometimes we just don’t know. We move on.
Nonetheless, a few moments’ confusion notwithstanding, a delightful outcome: Our bitmaps are imported and our invaders are animated.
Life is good!
See you next time!