Now that I've refactored the code, I'll try adding three new features: multiple bullets, a shotgun powerup and a UI change to show the remaining time for each powerup.
Adding support for multiple bullets
(These changes can be seen in the github history.) (Note that I accidentally checked in a lot of other files with this check in, please ignore them.)
First, the multiple bullets. I've learned from the code that the GameWorld object has a
bullet member that shows the current location of the single bullet in the world. It's set to a
Bullet object when there's a bullet on the map or None when there's no bullet anywhere. I'm going to change this to a list data structure, which can hold multiple
Bullet objects (or be an empty list if there are no bullets on the map.) I'll rename it from
bullets to reflect the multiple-bullet nature.
Any place where the
bullet member is set to None (to delete the bullet) will instead either be set to
 or have a
del statement called on a
Bullet item in the list. Any place where the
bullet member was set to a
Bullet object will be changed instead to have an
append() method call to add a new
Bullet object to the list.
Now that there can be more than one bullet on the map, we have to change the
if statement that checks if the bullet has collided with a bubble to a loop that checks each bullet for colliding with a bubble. We will change this line:
...to these lines:if self.bullet != None and self.bullet.collides_with(b):
for i in range(len(self.bullets) - 1, -1, -1): if self.bullets[i].collides_with(b):
Note that in an earlier change, we modified the
shoot_at() method of the
Ship class to return a list of
Bullet objects rather than just a single
Bullet object. This was done in anticipation of this future multi-bullet feature. We need to modify the caller of
shoot_at() to append all of the
Bullet objects to the
world.bullet list. That is where this code comes from:
if len(world.bullets) < 5: world.bullets.extend(world.ship.shoot_at(x / float(MAP_WIDTH), y / float(MAP_HEIGHT)))
extend() list method is kind of like
append(), except it appends all the items in a list instead of appending just one value.
len(world.bullets) < 5 prevents new bullets from being added if there are five or more bullets currently on the map. (You can change this value to anything you want, but I wanted to prevent the player from just spamming the map full of bullets.
Checking these changes in with the git log message, "Add support for multiple bullets."
Add a shotgun powerup
The shotgun powerup will have the ship fire multiple spread-out shots each time the mouse button is clicked. To get an idea of what code will be needed for this powerup, let's look at what code implements the other (freeze, super bullet, shield) powerups:
- Code that draws the shotgun powerup icon in the
_shotgun_timermember for the
Shipclass to show how much time there is left for this powerup (and set to
0when the player doesn't have this powerup.)
- Code that decreases
_shotgun_timerover time in the
These changes are pretty much just copying the code used for the other powerups. We'll need to add some custom code to the shoot_at() method so that it returns multiple bullets if
True. Here's the new code that does this (and also replaces the previous code that only created a single
bullets =  for i in range(5): b = Bullet() b.pos.copy(self.pos); b.speed.x = x * 3 b.speed.y = y * 3 # Help out the poor sods who click on their # own ship and get stuck with a non-moving # bullet. (2009-11-14) if abs(x) < 0.1 and abs(y) < 0.1: b.speed.x *= 30 b.speed.y *= 30 if not self.has_shotgun(): return [b] # just return the one bullet b.speed.x += random.uniform(-0.15, 0.15) b.speed.y += random.uniform(-0.15, 0.15) bullets.append(b) return bullets
Note that even though we loop 5 times, if
False, we just return the first
Bullet object in a single-item list (since the caller is expecting a list of
Bullet objects, not just a single
Bullet object value.) Otherwise, a list of 5
Bullet objects will be returned.
Also notice that the
return statement for the non-shotgun case occurs before this code:
b.speed.x += random.uniform(-0.15, 0.15) b.speed.y += random.uniform(-0.15, 0.15)
This code alters the trajectory of the bullet very slightly. This is what causes the "spread" of bullets.
I'll check in this code with the log message, "Added the Shotgun powerup"
I'm going to make a slight improvement to the powerup icon for the shotgun blast by adding a couple lines so the icon looks more like the spread: Checked in with message, "Improved the shotgun powerup drawing"
Adding powerup timer UI
One of the things that struck me when I first started playing Square Shooter was that I didn't know what the power ups did at all. They just had these abstract looking icons, but I didn't really know which was which or what they did (the freeze was obvious, but square around my ship didn't obviously seem like a shield.)
Also, I'm not sure how long these power ups will last. In this change, we add a User Interface that displays the powerups we have as well as how much longer we will have them. In the green sidebar, we'll add the name of the powerup along with how many seconds it has remaining.
First, we'll need to change the
has_freeze() methods to return the amount of time they have left, rather than a simple boolean. WARNING! You should always be careful when changing either the parameters or the return value of a function. Anything that calls these functions might break. Be sure to do extensive testing before checking in this code.
This isn't too much of a concern for our case, because a zero value returned by these methods will be equivalent to
False and a non-zero value will be equivalent to
True. And also, this code is not part of a library that 3rd parties depend on. (Imagine how many people would have to change their code if Pygame decided to rename
Just to make sure that a negative value is returned from these methods (a negative float value is non-zero, so it will be equivalent to
True), we will also add a check for a negative value and return
0 in that case.
Here's the code that we add to the
render() method (which draws the other parts of the green sidebar):
text_y = 48 * 6 if self.world.ship: powerup_status = ((self.world.ship.has_shield(), 'Shield ' + str(int(self.world.ship.has_shield()))), (self.world.ship.has_super_bullets(), 'Super Bullet ' + str(int(self.world.ship.has_super_bullets()))), (self.world.ship.has_freeze(), 'Freeze ' + str(int(self.world.ship.has_freeze()))), (self.world.ship.has_shotgun(), 'Shotgun ' + str(int(self.world.ship.has_shotgun())))) for seconds_left, text in powerup_status: if seconds_left: text = self.msg_font.render(text, False, BLACK) self.screen.blit(text, (MAP_WIDTH + 20, text_y)) text_y += 25
First, we create a data structure that is a tuple of tuples which hold the number of seconds remaining for the powerup (so we can tell if the powerup is active or not) and a string with the powerup's name and the number of (whole) seconds left for it.
The rest of the code will render this text and blit it to the sidebar. The
text_y variable will increment whenever it writes out some text, so that the next bit of text will be below the previous one.
Checking in these changes with the log message, Add UI for timers, modified has_[powerup]() methods to return time left.
End of Square Shooter's makeover
That's all I'll do for now. With the code organized, making new features becomes slightly easier. I'll leave you with one last bit of advice: you might not always have to go back and refactor code. Code doesn't rust if you don't modify it: the Square Shooter game worked perfectly fine without bugs before I made these changes. Refactoring is for the benefit of human programmers, not the computer. Any time you make changes (even improvements) you run the risk of adding new bugs.
You only need to go back and refactor if it will make future changes easier to do. Of course, since code almost always has bugs, you are pretty much guaranteed to need to change it. (The code, as opposed to UI and feature, changes are covered in Part 1 and Part 2.)
Either way, I hope this blog series has given you ideas on how you can write cleaner and more readable code (and what makes code "clean" and "readable".) Keep an eye out for future Source Code Makeover blog posts, and thanks for reading!