Splitting Your Space Rocks Into Smaller Rocks
00:00 In the previous lesson, you made things crash into each other instead of floating on by. In this lesson, you’ll make your big rocks turn into little rocks and your little rocks into space pebbles. Bamm-Bamm would be proud.
00:13 In the current version of the game, when you hit a rock with a bullet, the rock goes away. As any fan of bad Bruce Willis movies will tell you, hitting a rock with a bullet isn’t going to make it just disappear.
00:24
Let’s break it into smaller pieces instead. To do this, your Rock
constructor will need to change. Right now, your rock positions are randomly generated, but if a rock is supposed to split into pieces, you don’t want that.
00:38 So the constructor needs to handle both the random appearance case for start conditions and the position specific case for rock splits. It will also need the ability to construct different sizes of rocks.
00:51
Let’s see some code. Here I am inside of models.py
. Let me scroll down to the Rock
object class.
01:01 What I’ve decided to do here is create a factory method. A factory method is a class method that constructs and returns an object. This pattern is useful if there are a bunch of different ways of constructing something.
01:13
Factory methods can sometimes make your code clearer. Instead of having to read all the various parameters to interpret how a constructor is going to be called, you can have a factory method called .create_random()
whose name might be considered clearer than having to use a use_random_position=True
parameter.
01:32 The end result is the same, and a matter of style and preference. If you’ve come to Python from other languages, especially Java, you’ll find there are far fewer factories in the snake-based coding world, but the occasional one used judiciously can still make your code easier to read.
01:48
The contents of this factory are pretty much a copy of the old .__init__()
method. A random position is created. It is checked if it is in a valid starting position. If so, the rock is constructed. If not, a new position is tried until a rock outside the minimum start gap is generated. Line 83 here is the end of the factory method, which actually creates the new rock with the random position and returns it.
02:12 Let me scroll down just a little bit.
02:18
The .__init__()
method has also changed. In addition to position, you now need to be concerned about the size of the rock being created. I’m going to use three sizes and an integer value of 1
, 2
, or 3
to indicate the size.
02:32 Normally, I’m adverse to using constants directly in the code. These are sometimes referred to as magic numbers because the source of their value might be considered magical.
02:41 Most of the time, you want to use a constant, instead, with a good name. In this case, though, I’ll make an exception, and you’ll see why in a second. Magic number or not, it needs to be stored in the class. And then based on this number, you need a scaling factor. This scaling factor is used to calculate the size of the rock. Large rocks will be the size currently in use, medium rocks will be half as big, and small rocks will be half as big again.
03:10 Line 94 scales the sprite based on the size just determined. Lines 97 through 99 determine the speed of the rock. This is the same random velocity code used before the change to different sized rocks. Let me scroll down just a little more.
03:27 I’ve added a method that takes a given rock and splits it. This method creates two new rocks in the same spot as the current object.
03:37
First, it checks if the rock is bigger than the smallest size. You don’t want to split rocks of size 1
. Next, as you’re impacting the number of rocks in the game, you have to access the thing containing the rocks. Before now, the container for rocks lived inside of the SpaceRocks
game object, same as the bullets.
03:57 This is a good place to have a conversation about the trade-offs of global variables. The standard advice to new programmers is not to use global variables.
04:06 Encapsulation is a good thing. It tends to organize your code in a more readable fashion. That being said, if you need to access something that is encapsulated, you always have to pass that something around.
04:18
You saw the consequence of this earlier when the reference to the bullet container had to be added to the Spaceship
class. To show you a different way of thinking about the same problem, I’ve changed the way the code stores rocks and bullets.
04:31
I’ve made them global variables inside of the game.py
module. You can decide for yourself which you think is more readable: breaking the rules about encapsulation or passing the container around every time you need it.
04:44
Making this a global variable means you can access it by importing the module. The problem is the game.py
module imports the models.py
module, the file you’re in right now.
04:55
Having this file import something from game.py
is what’s called a circular import. Python won’t let you do this. This is why this import is inside of this method on line 105 instead of at the top of the file. If the import had happened when the module was loaded the first time, there would be a circular import and the compiler would complain.
05:16 Putting it inside of this method is fine though. The module is already loaded into memory, so this code simply accesses the already-loaded module. Circular import problem resolved. Now, what was I talking about?
05:30 Oh yeah, splitting rocks. Lines 107 and 108 are where two new rocks are generated. They are both generated in the exact same space as the current rock. Note that the value for size is one less than the current size.
05:45
This is why I was okay with using the magic numbers 1
, 2
, and 3
for the rock sizes. It makes generating the next smaller rock a simple subtraction operation.
05:55
If I had used a constant or an enum instead of a magic number, this would end up having to be an if
/else
condition block. Like a lot of coding, this is a trade-off. Magic numbers are harder to read, but in this case, it gives you easier-to-read splitting code. A better compromise would be to add some comments around the use of the magic numbers. You should do that. Comments are good.
06:23
All right. This is game.py
. The first change in here is turning the bullet and rock containers into global variables as promised. Anywhere where you had self.rocks
or self.bullets
in the SpaceRocks
object will now need to change to a global reference.
06:39
And here’s the first case of that. global rocks
sounds like a bad name for a band, but what it is actually doing is telling Python you want to use the global variable instead of creating a new overriding local one. With the global established, the rock generation code is similar.
06:55
It is still creating six rocks inside of a list comprehension, but now the .create_random()
rock factory is being used instead of calling for a new object. Scrolling down,
07:07
here’s the .game_objects()
property. And once again, because of the change to global variables, this method has to be updated. I’m creating a temporary list here with all the bullets and rocks.
07:19
And because our ship now might have gone away, it should only be included with the game objects if it is still there. With the .game_objects()
property up-to-date, now the collision logic needs to change. Line 66 indicates that bullets and rocks will be global, and then line 79 is what removes the rock when a collision between a rock and a bullet is found.
07:42
Now that the rock is removed, it can be replaced with two rocks through the call to .split()
. Remember that .split()
has a check in it for tiny rocks. If the rock is too small, split does nothing, so it is safe to call it either way.
07:57 The remaining code is the same as before: removing the bullet and carrying on. Let’s break some rocks, hot sun optional. Kudos to the tiny little subset of Python coders with the appropriate taste in well-aged music that got that reference. I suspect it was one of you.
08:14 Might’ve only been my editor in fact, but it made me smile. So, moving on.
08:20 Here I am, recovered from my inside joke tangent, flying my spaceship again.
08:27 Pew, pew! Look at that, smaller rocks! And there you go, three rock sizes. And when the third one is hit, it disappears. I won’t tell you how many takes this took.
08:38 It’s time to make some noise. Next up, you’ll add sound to your game.
blank on Nov. 13, 2021
sorry that should say the list in models.py is empty
Christopher Trudeau RP Team on Nov. 14, 2021
Hi @blank,
It is hard to debug code without seeing it, but the first thing I would double check is that you declared bullets as global
inside of SpaceRocks.game_objects()
. If you missed that it defaults to a local variable and the global variable wouldn’t get populated.
If it isn’t that, take a look at the sample code download in the s14_split
folder and see what differences you can find between it and your code.
Good luck. Bug hunting always sucks :)
blank on Nov. 16, 2021
I know it would be long but should I post my code. It looks like the list are just local variables. I tried to both check my code vs the example and also ended up copying the example code but ran into the same issue were neither the Bullets or Astroid lists worked as global variables.
I know it would be long but is there a way to post my code?
Bartosz Zaczyński RP Team on Nov. 16, 2021
@blank If you have a GitHub account, then you could post your code to a GitHub gist and share its link here.
blank on Nov. 17, 2021
Ok so here is what I have: github.com/theeviru/pygam_astroids
I’m sure it’s something simple but from both the example code and what I have the spaceship loads and so do the asteroids but neither of the global variables are loading in. On the module.py
it acts like they are just local lists.
blank on Nov. 17, 2021
Sorry, that should be modules.py
.
blank on Nov. 17, 2021
Well, just found out the issue I was running the lines of code:
from game import SpaceRocks
if __name__ == "__main__":
space_rocks = SpaceRocks()
space_rocks.main_loop()
Inside of the game.py
and I’m guessing because of this it was causing the global variables for the bullets and astroids to not work correctly.
Thanks for the help!
Bartosz Zaczyński RP Team on Nov. 17, 2021
@blank We’re always here to help you in rubber duck debugging of your code 🙂
Christopher Trudeau RP Team on Nov. 17, 2021
Hi @blank,
Your bug has taught me something! Python isn’t quite doing what I expected here.
What you’ve run into has to do with when (and how often) a module gets imported.
By using __main__
, game gets imported exactly once. When it is imported bullets gets initialized.
Putting the __name__
check in game.py does something different though. When you run “python game.py” it loads the module to execute, but when models.py imports game.py it is being loaded a second time. This second load of game.py is re-intializing the global bullets variable, wiping it out.
You can see the effect by using the id()
function. In case you haven’t seen it before, id()
takes an object and gives the identifier that the interpreter has assigned to it. With a couple of print()
statements in the code showing id(bullets)
you’ll see that the id changes – the second initialization is creating a new list, which is a new object, overwriting the original.
Quite honestly, this is not behaviour I would have expected. This just reminds me that I should always be careful with module level variables and how modules get loaded.
JCode888 on June 24, 2023
Hello! Great tutorial Christopher!
I have a question. I am attempting to make this look a lot more like a classic game of Asteroids. I have created a ship, asteroids and bullets in Adobe Illustrator and exported them as PNGs to use as my assets. They basically just have the white outline as in the original game.
I notice when the asteroids split, the code that scales them, scales the outline of my old-school asteroids. The outlines on the smaller asteroids are then hard to see, making it look like the asteroids are missing sides.
Any thoughts on how to get around this? I suppose I could increase the 0.5 stroke that I used for the outline around the asteroids in Illustrator…but then the outlines will look a little “chunky” on the full-size asteroids and not as much like the original.
Thanks in advance for any help you (or anyone) can offer!
Christopher Trudeau RP Team on June 25, 2023
Hi JCode888,
I’m glad you enjoyed the tutorial. The original Asteroids game wasn’t pixel based but vector based, those lines were drawn by the machine instead of scaled out of an image. The techniques for coding that kind of game are rather different, and used very much anymore.
One possible solution for you to get that look would be to have several images for your asteroids, each a different size. Instead of writing scaling code you would blit a different graphic onto the screen.
JCode888 on June 26, 2023
Thanks again for the help Christopher! I’ll play around with it and see what I can do!
Become a Member to join the conversation.
blank on Nov. 13, 2021
not sure I something is typed wrong but when i use the global variable of bullets and rocks in the Game.py when I try to import them into models.py the lit in models.py is empty.