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.

_images/Rocket.png

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()