07 Rocket Launch¶
Using the Keyboard to Launch a Rocket - Part 1¶
Now that you can save and load worlds that are made out of the new high tech composite blocks, let’s put those skills to use to launch a rocket! Rockets are amazing machines that the USAF uses to lift satellites to orbit.
For this exercise we will use the previous exercise 06 Loading the World to create a rocket similar to the rocket on the cover art for SensorCraft. You can either use only the composite blocks and the previous exercise 06 Loading the World to build a rocket of your own design or you can use the supplied text file called “rocket.txt” in the code directory.
To get started with this programming exercise first copy the file 06_reading_the_saved_world.py python code to a new file 07_rocket_launch_TVR.py (again replacing TVR with your initials) using the following command:
cp 06_reading_the_saved_world.py 07_rocket_launch_TVR.py
If you are going to use the provided rocket in “rocket.txt,” you need to change
the file loaded in the load_txt
function. If you want to load this file,
change the line that reads with open('composite_world.txt', 'r') as file:
to say with open('rocket.txt', 'r') as file:
so now when you load from a
file, it will look for the file called “rocket.txt”. The code should look as
shown below:
def load_txt(self):
"""
Load composite blocks from a .txt file
"""
with open('rocket.txt', 'r') as file:
line = file.readline()
while line:
line = line.split(" ")
x, y, z = int(line[0]), int(line[1]), int(line[2])
block_type = self.get_block_index(num=int(line[3]))
self.add_block((x, y, z), block_type)
line = file.readline()
Now we need to create a new variable within the “Model” class that helps the
class keep track of whether the rocket has been loaded or not. This will
prevent a player from loading a rocket multiple times. Jump down to line
167 (the end of the Model __init__
function before the line
self._initialize()
) in the new file and add the code shown below:
# True/False variable that tells the model class if the rocket has already
# been loaded. This prevents it from being loaded twice.
self.rocket_loaded = False
Next jump down to line 463 (just after load_txt
) and create a new method
called “move_rocket_up” which will move the rocket up one block. The new
“move_rocket_up” method is shown below:
def move_rocket_up(self):
"""
0) find all the composite blocks and add to composite_world
dictionary. At the same time erase all the composite blocks from the world
1) loop through composite_world dictionary and add blocks with y + 1
to add each block to the world.
"""
composite_world = {}
for world_key, world_value in list(self.world.items()):
if (world_value == COMPOSITE_RED or
world_value == COMPOSITE_BLUE or
world_value == COMPOSITE_BLACK or
world_value == COMPOSITE_GREY or
world_value == COMPOSITE_GREEN):
composite_world[world_key] = world_value
self.remove_block(world_key, True)
for composite_key, composite_value in composite_world.items():
if (composite_value == COMPOSITE_RED or
composite_value == COMPOSITE_BLUE or
composite_value == COMPOSITE_BLACK or
composite_value == COMPOSITE_GREY or
composite_value == COMPOSITE_GREEN):
new_x = composite_key[0]
new_y = composite_key[1] + 1
new_z = composite_key[2]
self.add_block((new_x, new_y, new_z), composite_value)
Let’s walk through this function a little more closely. First, we create a new
dictionary composite_world
. This will be used to store the location and
type of all composite blocks, that is it will store all the blocks that make
up the rocket.
Next is the first for loop. We need to go through every block in the world and
identify and save all composite blocks into composite_world
. Notice in the
for loop there are two variables: world_key
and world_value
. Our world
dictionary is stored so that the keys are the location of the block in the form
(x, y, z)
and the value is the type of block. So what we want to do is see
if world_value
is one of our composite blocks.
When we find a composite block, we store it’s location and type into
composite_world
with composite_world[world_key] = world_value
and then
remove the block with self.remove_block(world_key, True)
.
Now we need to go back and add every block back, but moved one block up. So, we
go through composite_world
just as we did before, but now we update the y
component of the key with new_y = composite_key[1] + 1
. Python uses zero
indexing so if we want to access the second element y
of (x, y, z)
we
have to use composite_key[1]
. Finally we can actually add the block back
with a call to add_block
.
We also need the rocket to come back down to land. At line 491 after
move_rocket_up
add a new method called move_rocket_down
which will move
the rocket down one block. The new move_rocket_down
method is shown below:
def move_rocket_down(self):
"""
0) find all the composite blocks and add to composite_world
dictionary. At the same time erase all the composite blocks from the world
1) loop through composite_world dictionary and add blocks with y - 1
to add each block to the world.
"""
composite_world = {}
for world_key, world_value in list(self.world.items()):
if (world_value == COMPOSITE_RED or
world_value == COMPOSITE_BLUE or
world_value == COMPOSITE_BLACK or
world_value == COMPOSITE_GREY or
world_value == COMPOSITE_GREEN):
composite_world[world_key] = world_value
self.remove_block(world_key, True)
for composite_key, composite_value in composite_world.items():
if (composite_value == COMPOSITE_RED or
composite_value == COMPOSITE_BLUE or
composite_value == COMPOSITE_BLACK or
composite_value == COMPOSITE_GREY or
composite_value == COMPOSITE_GREEN):
new_x = composite_key[0]
new_y = composite_key[1] - 1
new_z = composite_key[2]
self.add_block((new_x, new_y, new_z), composite_value)
Notice move_rocket_up
and move_rocket_down
are almost
identical, the only difference being that to move the rocket up
we increase the y component whereas to move it down we decrease it.
Next we need to make sure the position and rotation of Dr. Steve is correct
so when the rocket appears we will be looking at the rocket and not somewhere
else. Feel free to adjust these numbers as you see fit: the rotation and
position variable that we use in this guide are just suggestions. A little
experimentation goes a long way when setting position and rotation variables.
On lines 539 - 549 of the Window constructor in the method called __init__
change the self.position
and self.rotation
variables as shown below:
# Current (x, y, z) position in the world, specified with floats. Note
# that, perhaps unlike in math class, the y-axis is the vertical axis.
self.position = (-35, 0, 0)
# First element is rotation of the player in the x-z plane (ground
# plane) measured from the z-axis down. The second is the rotation
# angle from the ground plane up. Rotation is in degrees.
#
# The vertical plane rotation ranges from -90 (looking straight down) to
# 90 (looking straight up). The horizontal rotation range is unbounded.
self.rotation = (90, 0)
The last thing we need to do is modify the familiar method “on_key_press” so when the “I” key is pressed the “move_rocket_up” method is called and when the “K” key is pressed the “move_rocket_down” method is called. The new “on_key_press” method is given below:
def on_key_press(self, symbol, modifiers):
""" Called when the player presses a key. See pyglet docs for key
mappings.
Parameters
----------
symbol : int
Number representing the key that was pressed.
modifiers : int
Number representing any modifying keys that were pressed.
"""
if symbol == key.W:
self.strafe[0] -= 1
elif symbol == key.S:
self.strafe[0] += 1
elif symbol == key.A:
self.strafe[1] -= 1
elif symbol == key.D:
self.strafe[1] += 1
elif symbol == key.SPACE:
if self.dy == 0:
self.dy = JUMP_SPEED
elif symbol == key.ESCAPE:
self.set_exclusive_mouse(False)
elif symbol == key.Y:
self.position = (0, 0, 0)
dx, dy, dz = self.get_motion_vector()
self.dy = 0
elif symbol == key.TAB:
self.flying = not self.flying
elif symbol in self.num_keys:
index = (symbol - self.num_keys[0]) % len(self.inventory)
self.block = self.inventory[index]
elif symbol == key.B:
self.buildWall()
elif symbol == key.I:
self.model.move_rocket_up()
elif symbol == key.K:
self.model.move_rocket_down()
elif symbol == key.L:
self.model.load_txt()
Notice that the new methods “move_rocket_up” and “move_rocket_down” are both in the class called “Model”. This is expected because manipulating blocks is done by changing the world data dictionary and it is located within the model class.
Test the rocket! Try launching it. It is fun to use the “K” key to make the rocket go beneath the ground.
Using the Computer Clock to Launch a Rocket - Part 2¶
Part 1 used the “L” key and the “K” key to make the rocket go up and down. While this is a good start it is not very realistic. In part 2 we are going to make the rocket go up and down based on the SensorCraft update loop and your computer’s built in clock.
In programming a common approach is to take a complicated problem and break it down into much smaller pieces, then solve each of those pieces individually. A common phrase is “How do you eat an elephant? One bite at a time”. We started by making the rocket move, now we’re going to make it move on its own.
First the model class needs several new variables added to the code so move down to line 164 and replace the code we just changed here with the following:
# True/False variable that tells the model class if the rocket has already
# been loaded. This prevents it from being loaded twice.
self.rocket_loaded = False
# True/False variable that tells the model class if the rocket has been
# launched.
self.rocket_launched = False
# stores the altitude of the rocket
self.rocket_altitude = 0
# mode of the rocket "up" for going up and "down" for going down
self.rocket_mode = "up"
# Counts the number of calls to rocket_update. This variable will
# help us slow down the update rate and keep frame rate high
self.rocket_update_count = 0
One of the new variables we added was rocket altitude. This is simply a number
of blocks where an altitude of zero represents the starting point. Recall for
our coordinate system in SensorCraft “Y” represents height above the ground.
Next on line 476 you want to add a line that will increase the altitude one
block every time the method “move_rocket_up” is called. The definition of
move_rocket_up
should now be the following:
def move_rocket_up(self):
"""
0) find all the composite blocks and add to composite_world
dictionary. At the same time erase all the composite blocks from the world
1) loop through composite_world dictionary and add blocks with y + 1
to add each block to the world.
"""
self.rocket_altitude += 1
composite_world = {}
for world_key, world_value in list(self.world.items()):
if (world_value == COMPOSITE_RED or
world_value == COMPOSITE_BLUE or
world_value == COMPOSITE_BLACK or
world_value == COMPOSITE_GREY or
world_value == COMPOSITE_GREEN):
composite_world[world_key] = world_value
self.remove_block(world_key, True)
for composite_key, composite_value in composite_world.items():
if (composite_value == COMPOSITE_RED or
composite_value == COMPOSITE_BLUE or
composite_value == COMPOSITE_BLACK or
composite_value == COMPOSITE_GREY or
composite_value == COMPOSITE_GREEN):
new_x = composite_key[0]
new_y = composite_key[1] + 1
new_z = composite_key[2]
self.add_block((new_x, new_y, new_z), composite_value)
We should make the same change for the corresponding function
move_rocket_down
which will decrease the altitude by one block every time
the method is called:
def move_rocket_down(self):
"""
0) find all the composite blocks and add to composite_world
dictionary. At the same time erase all the composite blocks from the world
1) loop through composite_world dictionary and add blocks with y - 1
to add each block to the world.
"""
self.rocket_altitude -= 1
composite_world = {}
for world_key, world_value in list(self.world.items()):
if (world_value == COMPOSITE_RED or
world_value == COMPOSITE_BLUE or
world_value == COMPOSITE_BLACK or
world_value == COMPOSITE_GREY or
world_value == COMPOSITE_GREEN):
composite_world[world_key] = world_value
self.remove_block(world_key, True)
for composite_key, composite_value in composite_world.items():
if (composite_value == COMPOSITE_RED or
composite_value == COMPOSITE_BLUE or
composite_value == COMPOSITE_BLACK or
composite_value == COMPOSITE_GREY or
composite_value == COMPOSITE_GREEN):
new_x = composite_key[0]
new_y = composite_key[1] - 1
new_z = composite_key[2]
self.add_block((new_x, new_y, new_z), composite_value)
Let’s add two methods at the end of the model class after move_rocket_down
:
def launch_rocket(self):
if self.rocket_loaded and not(self.rocket_launched):
self.rocket_launched = True
self.rocket_mode = "up"
def process_rocket(self):
"""
This function will process the rocket and decide if it should
move up or down
"""
if self.rocket_loaded and self.rocket_launched and self.rocket_update_count >= 8:
# adjust the rocket
if self.rocket_mode == "up":
self.move_rocket_up()
else:
self.move_rocket_down()
# check the mode of the rocket
if self.rocket_altitude >= 40:
self.rocket_mode = "down"
elif self.rocket_altitude < 1:
self.rocket_mode = "up"
self.rocket_launched = False
self.rocket_update_count = 0
else:
self.rocket_update_count += 1
The method “launch_rocket” is simple: it checks to make sure the rocket
is loaded, meaning the user pressed the “L” key, and the rocket is not
launched. If both of those conditions are met then rocket_launched
is set
to true and the rocket_mode
is set to “up”.
The method “process_rocket” is more complicated but still easy to understand.
First it checks for three conditions: rocket_loaded
, rocket_launched
,
and rocket_update_count
>= 8. rocket_update_count
is used to slow down
the updates of the rocket because the update method in the Window class is called
more frequently than we need to update the rocket. If all three of the previous
conditions are met then the method will call move_rocket_up
or
move_rocket_down
based on the rocket_mode
. At the end of the
process_rocket
method it checks to see if the modes need to be changed
depending on the value of rocket_altitude
.
Next we have to update the familiar method on_key_press
to use the
new methods we just created in the model class:
def on_key_press(self, symbol, modifiers):
""" Called when the player presses a key. See pyglet docs for key
mappings.
Parameters
----------
symbol : int
Number representing the key that was pressed.
modifiers : int
Number representing any modifying keys that were pressed.
"""
if symbol == key.W:
self.strafe[0] -= 1
elif symbol == key.S:
self.strafe[0] += 1
elif symbol == key.A:
self.strafe[1] -= 1
elif symbol == key.D:
self.strafe[1] += 1
elif symbol == key.SPACE:
if self.dy == 0:
self.dy = JUMP_SPEED
elif symbol == key.ESCAPE:
self.set_exclusive_mouse(False)
elif symbol == key.Y:
self.position = (0, 0, 0)
dx, dy, dz = self.get_motion_vector()
self.dy = 0
elif symbol == key.TAB:
self.flying = not self.flying
elif symbol in self.num_keys:
index = (symbol - self.num_keys[0]) % len(self.inventory)
self.block = self.inventory[index]
elif symbol == key.B:
self.buildWall()
elif symbol == key.I:
self.model.launch_rocket()
elif symbol == key.L:
self.model.load_txt()
Finally, we have to modify the update method in the window class. Since
different speed computers exist, one common technique used in simulations is to
base events on time. SensorCraft is set to call the update method in the
window class 1.0 / TICKS_PER_SEC or 1/60th of a second in the window class
__init__
method. All we have to do is call the process_rocket method in the
model class using the code below on line 688 (after the get_motion_vector
function):
def update(self, dt):
""" This method is scheduled to be called repeatedly by the pyglet
clock.
Parameters
----------
dt : float
The change in time since the last call.
"""
self.model.process_queue()
sector = sectorize(self.position)
if sector != self.sector:
self.model.change_sectors(self.sector, sector)
if self.sector is None:
self.model.process_entire_queue()
self.sector = sector
m = 8
dt = min(dt, 0.2)
for _ in xrange(m):
self._update(dt / m)
self.model.process_rocket()
Finally, we need to update what happens when the rocket is loaded from the file.
Go back to the load_txt
function defined previously and change its implementation
to read as follows:
def load_txt(self):
"""
Load composite blocks from a .txt file
"""
self.rocket_loaded = True
with open('rocket.txt', 'r') as file:
line = file.readline()
while line:
line = line.split(" ")
x, y, z = int(line[0]), int(line[1]), int(line[2])
block_type = self.get_block_index(num=int(line[3]))
self.add_block((x, y, z), block_type)
line = file.readline()
Now when you run the program, you should be able to press ‘L’ to load the rocket
from the file and then press ‘I’ to launch the rocket. Watch as it flies away!
To save US tax payers money the rocket will automatically land after it reaches
a altitude greater than or equal to 40. You can see the code required to make
that work in the process_rocket
method.
Dr. Steve Rides the Rocket - Part 3¶
In part 1 and part 2 of the rocket launch, Dr. Steve was on the ground while
the rocket was launched. Now we are going to put Dr. Steve on top of the rocket
so that he rides the rocket. First, we are going to add to the update method on
line 714 (at the end of the update
function of the model class) so that Dr.
Steve goes up as the rocket goes up.
def update(self, dt):
""" This method is scheduled to be called repeatedly by the pyglet
clock.
Parameters
----------
dt : float
The change in time since the last call.
"""
self.model.process_queue()
sector = sectorize(self.position)
if sector != self.sector:
self.model.change_sectors(self.sector, sector)
if self.sector is None:
self.model.process_entire_queue()
self.sector = sector
m = 8
dt = min(dt, 0.2)
for _ in xrange(m):
self._update(dt / m)
self.model.process_rocket()
if self.model.rocket_launched:
""" 10 is the x position of the rocket. 19 is the height of the
rocket. 2 is the z position of the rocket.
"""
self.position = (10, self.model.rocket_altitude + 19, 2)
The new position is where Dr. Steve will go when the rocket is launched, which
is on top of the rocket. These few lines of code keep Dr. Steve firmly planted
on top of the rocket and prevents Dr. Steve from falling off the rocket.
Finally, we need to modify the on_key_press
method so that when you press
“I” to launch the rocket, Dr. Steve is put on top of the rocket. The method of
on_key_press
should now read as follows:
def on_key_press(self, symbol, modifiers):
""" Called when the player presses a key. See pyglet docs for key
mappings.
Parameters
----------
symbol : int
Number representing the key that was pressed.
modifiers : int
Number representing any modifying keys that were pressed.
"""
if symbol == key.W:
self.strafe[0] -= 1
elif symbol == key.S:
self.strafe[0] += 1
elif symbol == key.A:
self.strafe[1] -= 1
elif symbol == key.D:
self.strafe[1] += 1
elif symbol == key.SPACE:
if self.dy == 0:
self.dy = JUMP_SPEED
elif symbol == key.ESCAPE:
self.set_exclusive_mouse(False)
elif symbol == key.Y:
self.position = (0, 0, 0)
dx, dy, dz = self.get_motion_vector()
self.dy = 0
elif symbol == key.TAB:
self.flying = not self.flying
elif symbol in self.num_keys:
index = (symbol - self.num_keys[0]) % len(self.inventory)
self.block = self.inventory[index]
elif symbol == key.B:
self.buildWall()
elif symbol == key.I:
self.model.launch_rocket()
if self.model.rocket_loaded:
self.position = (10, 20, 2)
elif symbol == key.L:
self.model.load_txt()