How to make a Text Adventure game in Rust - IX - Adding a Game File - Part III

In this project, we're making a fully functional text adventure game from scratch. The post assumes you've read the earlier posts in the series. To get caught up jump to the first post and start at the beginningThis project is heavily based on the work of Ruud Helderman and closely follows the content and structure of his excellent tutorial series on the same topic using the C language.

This content and all code is licensed under the MIT License.

Photo by Frank Cone

9 - Adding a Game File - Part III

This post is a bonus of sorts. In the last post we completed moving the game out of the source and into a file using the Serde crate. On review, the implementation achieved all of our goals,

  • the game is defined outside of the source code in a file,
  • the game file is in a format we wanted,
  • the game file is easily editable, and
  • the game file will easily allow us have many fields on objects.

BUT...

On a closer look there are issues with the current solution that can be easily remedied with existing Serde features.

Better Error Checking

We'll be editing the game file by hand using a text editor. And try as we might, we'll make mistakes. Serde provides checks out of the box, but we can do better using Serde attributes to annotate our code and provide guidance to include additional error checking.

Serde attributes, when added to the source code, control how Serde macros operate when generating code. For the game file, we can add the deny_unknown_fields attribute. As indicated by its name, deny_unknown_fields will trigger Serde to generate errors if unknown fields are encountered when deserializing. This looks like this:

// --snip--

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SavedObject {
	// --snip--
}

Optional Values

Another opportunity is to look at what fields are required for the object definitions in the game file. As is, the game file will need definitions for all fields of the object struct. BUT, for many objects (such as a game world location like the bridge), the location and destination fields will be empty (represented as None in the Object struct, and "" in the SavedObject struct and game_file.ron. These empty fields take up space in the game file and mean that for all objects we'll need to define all fields even if they are not used. We can do better.

Serde has a field attribute default that will cause generated deserializers to set undefined values to the value defined by Default::default() trait implemented on the type. Rust provides reasonable defaults for all simple types, and we can define our own defaults for custom types by implementing the Default trait.

The Default::default() value for String types is the empty string, which means that by adding the default attribute to the location and destination fields, we'll get reasonable default values if we just leave their definitions out of the game file entirely. This simplifies the game file, enhancing readability and editability.

In addition to the default attribute, there is also a default = "path" attribute that we can use if we want some other default value. This will become important in the next post as we talk about adding attributes to the Object struct. For example, we won't want the default weight to be 0 (the default value for an int). But the alternative would mean listing a weight for all objects. With default = "path" we'll be able to set a reasonable default weight for many Object fields and override when needed.

Putting it together

We can look now at changes we can make to the game to take advantage of these attributes. Updated versions of the SavedWorld and SavedObject structs are all we need:

// --snip--

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SavedObject {
    pub labels: Vec<String>,
    pub description: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub location: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub destination: String,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SavedWorld {
    pub objects: Vec<SavedObject>,
}

These changes allow game_file.ron to be simplified as well.

// --snip--
World (
    objects : [
        (labels     : ["Bridge"],
        description : "the bridge",
        ),
        (labels     : ["Galley"],
        description : "the galley",
        ),
        
        // --snip--
        
        (labels     : ["Yourself"],
        description : "yourself",
        location    : "Bridge",
        ),
        
        // --snip--
        
        (labels     : ["Aft"],
        description : "a passage aft to the galley",
        location    : "Bridge",
        destination : "Galley"
        ),
        
        // --snip--
        
    ],
)

Progress

These simple changes add a lot to our implementation. We'll now be more likely to catch eding errors when modifying the game file, and also have a much simpler file to edit as we build in more attributes.

In the next post, we'll look at adding additional attributes to game objects that will further enhance the player's experience and interactivity in the game.

⇂  View Source

Enjoyed this post? Help me create more with a coffee. Never miss out on future posts by following me.

Comments

Popular posts from this blog

How to make a Text Adventure game in Rust - Introduction