05 Saving the World¶
SensorCraft makes it fun to build objects in your world but we currently have no way to save them for use in other worlds. For this exercise, we are going to add some new textures and create a new method that will save the world so we can reload it in the future. The blocks we are going to add will be state of the art United States Air Force (USAF) created blocks that are made out of composite material. Composites are now used in aircraft like the F22 Stealth Fighter and commercial aircraft. To get started with this programming exercise, first copy the file 03_show_current_block_TVR.py python code to a new file 05_saving_the_world_TVR.py but replace TVR with your initials using the following command:
cp 03_show_current_block_TVR.py 05_saving_the_world_TVR.py
First, delete lines 77 - 87 as we no longer need the numbered stone blocks.
Next we need to change the file name used for textures on line 76 change the
line to TEXTURE_PATH = 'composite_textures.png'
.
At line 82 you want to add the new composite blocks so after all the changes
above line 76 - 89 should look like the following chunk of code:
TEXTURE_PATH = 'composite_textures.png'
GRASS = tex_coords((1, 0), (0, 1), (0, 0))
SAND = tex_coords((1, 1), (1, 1), (1, 1))
BRICK = tex_coords((2, 0), (2, 0), (2, 0))
STONE = tex_coords((2, 1), (2, 1), (2, 1))
COMPOSITE_RED = tex_coords((3, 1), (3, 1), (3, 1))
COMPOSITE_BLUE = tex_coords((3, 0), (3, 0), (3, 0))
COMPOSITE_BLACK = tex_coords((0, 2), (0, 2), (0, 2))
COMPOSITE_GREY = tex_coords((1, 2), (1, 2), (1, 2))
COMPOSITE_GREEN = tex_coords((2, 2), (2, 2), (2, 2))
COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]
Next, we need to remove the code that placed the numbered stone blocks to
form the number lines on the axis in exercise 3. Your _initialize
method
should look like the following after all the code has been deleted:
def _initialize(self):
""" Initialize the world by placing all the blocks.
"""
n = 80 # 1/2 width and height of world
s = 1 # step size
y = 0 # initial y height
for x in xrange(-n, n + 1, s):
for z in xrange(-n, n + 1, s):
# create a layer stone an grass everywhere.
self.add_block((x, y - 2, z), GRASS, immediate=False)
self.add_block((x, y - 3, z), STONE, immediate=False)
if x in (-n, n) or z in (-n, n):
# create outer walls.
for dy in xrange(-2, 3):
self.add_block((x, y + dy, z), STONE, immediate=False)
We now have to add the new composite blocks to our character’s inventory. To do so, modify line 490 so it looks like the following:
# A list of blocks the player can place. Hit num keys to cycle.
self.inventory = [BRICK, GRASS, SAND, COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK, COMPOSITE_GREY, COMPOSITE_GREEN]
Recall back to the exercise 03_show_current_block
and consider what modifications need to be made to the draw_label
method.
Within the if/else block, you need to make adjustments for the new composite
blocks that we added your new draw_label
method should be something like:
def draw_label(self):
""" Draw the label in the top left of the screen. Label includes
the current block selection.
"""
# determines the current block selected
if self.block == BRICK:
blockSelectedString = "Brick"
elif self.block == GRASS:
blockSelectedString = "Grass"
elif self.block == SAND:
blockSelectedString = "Sand"
elif self.block == COMPOSITE_RED:
blockSelectedString = "Composite Red"
elif self.block == COMPOSITE_BLUE:
blockSelectedString = "Composite Blue"
elif self.block == COMPOSITE_BLACK:
blockSelectedString = "Composite Black"
elif self.block == COMPOSITE_GREY:
blockSelectedString = "Composite Grey"
elif self.block == COMPOSITE_GREEN:
blockSelectedString = "Composite Green"
x, y, z = self.position
self.label.text = '%02d (%.2f, %.2f, %.2f) %d / %d Block: %s' % (
pyglet.clock.get_fps(), x, y, z,
len(self.model._shown), len(self.model.world),
blockSelectedString)
self.label.draw()
How do we tell the game that it can save the world? We could spend time and
write a complicated menu system that lets a user enter a file name, but since
we are just starting out lets deploy a simple solution. One simple solution
is to simply press a key, in this case the O
key and the method save_txt
will be called from the model class. Below is what the new on_key_press method
will look like, take note of the last elif:
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.TAB:
self.flying = not self.flying
elif symbol == key.Y:
self.position = (0, 0, 0)
dx, dy, dz = self.get_motion_vector()
self.dy = 0
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.O:
self.model.save_txt()
Finally, we implement the method that will save our world, or parts of the
world we care about. First a little background: Python has this incredibly
powerful data structure called a dictionary. Dictionaries allow a programmer
to associate a key to any given value. In our case the key is X
, Y
,
Z
position of the block and the value is the type of block like
COMPOSITE_RED
, COMPOSITE_BLUE
, COMPOSITE_BLACK
, SAND
,
GRASS
, BRICK
, etc. You can learn more about dictionaries in Python by reading the data structure dictionary documentation page.
Go to line 88 where you defined textured blocks and add another line as below:
COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]
The code should look like below:
TEXTURE_PATH = 'composite_textures.png'
GRASS = tex_coords((1, 0), (0, 1), (0, 0))
SAND = tex_coords((1, 1), (1, 1), (1, 1))
BRICK = tex_coords((2, 0), (2, 0), (2, 0))
STONE = tex_coords((2, 1), (2, 1), (2, 1))
COMPOSITE_RED = tex_coords((3, 1), (3, 1), (3, 1))
COMPOSITE_BLUE = tex_coords((3, 0), (3, 0), (3, 0))
COMPOSITE_BLACK = tex_coords((0, 2), (0, 2), (0, 2))
COMPOSITE_GREY = tex_coords((1, 2), (1, 2), (1, 2))
COMPOSITE_GREEN = tex_coords((2, 2), (2, 2), (2, 2))
COMPOSITE = [COMPOSITE_RED, COMPOSITE_BLUE, COMPOSITE_BLACK,
COMPOSITE_GREY, COMPOSITE_GREEN]
This creates a list that has all the types of composite blocks. These are the only types of blocks we are interested in saving, you could easily add more blocks to the list as is required for your game.
We want the file we save to to be as small as possible so we can share our
saved worlds with friends. Let’s think about the necessary information
needed to define a block. Every block has a location in the world in the
form x, y, z and a texture that defines how it looks. We have an indexed list
defining the textures so each composite block type has a corresponding number.
As we defined it, we can say COMPOSITE[0]
corresponds to COMPOSITE_RED
,
COMPOSITE[1] is COMPOSITE_BLUE
, and so on through the end of the list.
Hence we can store each block as four numbers: x, y, and z coordinates and the
number corresponding to the index in the list that has the block’s texture.
Let’s use this logic to write a new function that translates between the literal
texture type and the numerical equivalence in the COMPOSITE
list. At the end of
the model class copy and paste the below function.
def get_block_index(self, num="", type=""):
"""
If given a number (num), returns composite block at that index.
If given a composite block type (type), returns the index of that type.
"""
if not isinstance(num, str):
return COMPOSITE[int(num)]
elif not isinstance(type, str):
for i in xrange(0, len(COMPOSITE)):
if type == COMPOSITE[i]:
return i
print("Invalid Call")
This function behaves slightly differently than previous methods we’ve implemented.
In the first line we have two parameters num="", type=""
, but notice the
parameters are set equal to default values. So if you make a call to the function as
just model.get_block_index()
, these variables will default to ""
, n
empty string. The function isinstance
is a way of checking the type of
object. So now if we walk through the function, we can see that it checks if
num
is an integer and if it is, returns the texture at that index in the
COMPOSITE
list. If instead the type
argument is provided (the specific
texture), the function will return the corresponding integer index.
Now let’s add the function that actually writes to an external file to save our
blocks. Just above get_block_index
, add the following.
def save_txt(self):
"""
Save composite blocks to a .txt file
"""
file_name = 'composite_world.txt'
with open(file_name, 'w') as output:
for pos, type in self.world.items():
if type in COMPOSITE:
output.write(str(pos[0]) + " " + str(pos[1]) + " " + str(pos[2])
+ " " + str(self.get_block_index(type=type)) + "\n")
print("world is saved to file name: %s" % file_name)
This function finds all the composite blocks in the world and writes the
position and texture type as an integer to a file called
composite_world.txt
. If you open this file, you will see lines with four
numbers on them, the first three corresponding to a block’s position and the
last the index of the block’s texture. Run the program with IDLE then place a
few red composite bricks, finally push the O
key (not the zero key but the
O key) to save the composite blocks to a file called composite_world.txt
so
we can use the file in the next chapter to load a saved world. You can now
share these save files with friends via email attachment.