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 beginning. This 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.
Comments
Post a Comment