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:
.. literalinclude:: ../code/05_saving_the_world.py
:lines: 76-89
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:
.. literalinclude:: ../code/05_saving_the_world.py
:pyobject: Model._initialize
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:
.. literalinclude:: ../code/05_saving_the_world.py
:lines: 489-490
Recall back to the exercise `03_show_current_block <03_show_current_block.html>`__
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:
.. literalinclude:: ../code/05_saving_the_world.py
:pyobject: Window.draw_label
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:
.. literalinclude:: ../code/05_saving_the_world.py
:pyobject: Window.on_key_press
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:
.. literalinclude:: ../code/05_saving_the_world.py
:lines: 76-90
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.
.. literalinclude:: ../code/05_saving_the_world.py
:pyobject: Model.get_block_index
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.
.. literalinclude:: ../code/05_saving_the_world.py
:pyobject: Model.save_txt
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.