Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 65

Implementing Player Movement Functions

Recommended resources

Introduction

In our last lesson, you built the essential data structures for your game world, representing rooms and items using linked lists. This gave your world a tangible, albeit static, form. Now, we will breathe life into it by enabling the player to move within this world.

This lesson focuses on a core piece of text-adventure logic: player movement. We will go beyond the simple directional pointers from the previous exercise to implement a more robust and scalable system. You will learn to treat "passages" as first-class entities in your game and use a powerful concept known as "distance" to determine where the player can go. This professional-grade technique will make your go command intelligent and capable of providing meaningful feedback, moving your capstone project significantly closer to a real, interactive experience.


From Pointers to Data-Driven Passages

In the previous lesson, we connected rooms using direct pointers like room->north. This works for simple maps but quickly becomes inflexible. What if a passage isn't a cardinal direction, like "enter cave" or "cross bridge"? What if we want to add descriptions to passages?

A more powerful approach, common in classic text adventures, is to treat everything in the world—rooms, items, players, and even the connections between rooms—as a generic "object". Let's look at a map from a classic adventure game to see why this is useful.

A hand-drawn map for the text adventure "Treasures of Barboz." Notice the non-linear connections: "UP" to the top of a tree, a "BRIDGE" connecting two areas, and doors leading into a house.

To model a world like this, we'll refactor our data structures. Instead of separate Room and Item structs, we will create a single, unified OBJECT struct. This is a fundamental shift towards a data-driven design. A passage, like "east," will become an object located in the starting room, with a destination field pointing to the target room.


Designing a Unified Object Model

Let's begin by defining this new, all-encompassing OBJECT struct. For this, we'll draw inspiration from a fantastic C text adventure tutorial that solves this exact problem.

8. North, east, south, west

Read the introduction and review the code in the "object.h" and "object.c" sections of this chapter from Ruud Helderman's "How to program a text adventure in C".

Focus on the OBJECT struct definition in object.h. Note its members: description, tags, location, and destination. Then, examine how the game world is defined as an array of these OBJECTs in object.c. You can see how locations, items, the player, and passages (intoCave, exitCave) are all defined using the same structure. This is the model we will adopt.

The key innovations here are:

  • struct object *location;: This pointer indicates where an object is. For an item, it might point to a room. For a passage, it also points to the room it's in. For a room itself, this can be NULL.
  • struct object *destination;: This is used exclusively by passage objects to specify which room they lead to.
  • const char **tags;: This is a pointer to a NULL-terminated array of string pointers. This allows an object to have multiple names. For example, a passage might respond to both "east" and "entrance".

Here is the OBJECT struct we will use, adapted from the resource, which you should place in your world.h file (replacing your old Room and Item structs):

// In world.h

// Forward declaration
struct object;

typedef struct object {
    const char  *description;
    const char **tags;
    struct object *location;
    struct object *destination; // Used for passages
} OBJECT;

// Declare the array of all game objects
extern OBJECT objs[];

// Define macros for easy access to specific objects



#define player (objs + 0)
#define field (objs + 1)
#define cave (objs + 2)
// ... and so on for all your objects

The "Distance" System: A Scalable Approach to Interaction

With a unified object model, how do we implement the go command? A naive approach would be a series of if-else statements checking object types and locations. This becomes unmanageable quickly. A far more elegant solution is to create a single function that calculates the "distance" of any object from the player.

7. Distance

This resource explains the concept of "distance" for handling game logic. It's a powerful pattern we will implement.

First, read the section defining 'here' to understand the different proximity categories. Next, study the DISTANCE enum and the getDistance function in the sections DISTANCE enum and getDistance function. This function is the heart of our new system. Finally, see how these are declared and defined in misc.h and misc.c in the section starting with Time to put things into place. We will create similar files.

The core idea is to classify every object's relationship to the player into a set of discrete categories. The DISTANCE enum defines these categories, ordered from closest to farthest:

  • distSelf: The object is the player.
  • distHeld: The player is holding the object.
  • distHere: The object is in the same location as the player.
  • distOverthere: The object is a passage leading from the player's current location. This is the key for our go command.
  • distNotHere: The object exists but is not perceptible.
  • distUnknownObject: The input doesn't match any known object.

This system centralizes a huge amount of complex logic into one function. To move, we simply check if the destination's distance is distOverthere.


Implementing the go Command

Now we can combine our unified OBJECT model and the distance system to write a clean and powerful executeGo function.

7. Distance

This section shows the final implementation of the executeGo function using our new tools.

Examine the executeGo function in the location.c code block here. Pay close attention to the switch statement, which is the control structure that drives the logic. It elegantly handles all possible outcomes based on the distance to the target noun.

The logic is beautifully simple:

  1. Get the player's input (e.g., "east").
  2. Find the object corresponding to that noun (getVisible in the resource).
  3. Calculate its distance from the player using getDistance().
  4. Use a switch statement on the result:
    • If distOverthere, the move is valid. Update the player's location: player->location = obj->destination;.
    • For any other distance, provide an intelligent error message, like "You can't get much closer than this." or "You don't see any 'east' here."

This structure is not only clean but also extensible. The same getDistance function will later be used to handle get, drop, look at, and other commands.


Your Implementation Task

It's time to refactor your capstone project to use this new, more powerful architecture.

  1. Create misc.h and misc.c:

    • In misc.h, define the DISTANCE enum and declare the getDistance and getVisible functions.
    • In misc.c, implement the getDistance, objectHasTag, and getVisible functions based on the logic in resources LINK and LINK. You will need to adapt the getObject function from the resources into your getVisible helper. getVisible should find an object that matches a given tag and is within distOverthere.
  2. Refactor world.h and world.c:

    • In world.h, replace your previous structs with the new OBJECT struct definition and the macros for your game objects (player, field, etc.).
    • In world.c, define your game world as a static array of OBJECTs, similar to object.c from resource LINK. Create at least two rooms, a passage connecting them, and an item. For example:
      // In world.c
      
      
      
      #include "world.h"
      
      static const char *tags_field[] = {"field", NULL};
      static const char *tags_cave[] = {"cave", NULL};
      static const char *tags_passage[] = {"east", "entrance", NULL};
      // ... and so on
      
      OBJECT objs[] = {
          /* 0: player */     {"yourself", tags_yourself, field, NULL},
          /* 1: field */      {"an open field", tags_field, NULL, NULL},
          /* 2: cave */       {"a dark cave", tags_cave, NULL, NULL},
          /* 3: passage */   {"a cave entrance", tags_passage, field, cave},
          // ... more objects
      };
      
  3. Create location.h and location.c:

    • In location.h, declare void executeGo(const char *noun);.
    • In location.c, implement executeGo using the switch(getDistance(...)) pattern from resource LINK. When a move is successful, you must update the player's location (player->location = obj->destination;) and then call a function to describe the new location.
  4. Update game.c and main.c:

    • In game.c, modify parseAndExecute to call executeGo when the verb is "go".
    • In main.c, your main loop no longer needs to manage currentRoom. The player's location is now part of the global game state (player->location). The loop simply gets input and calls parseAndExecute.
    • Remember to #include all your new header files (misc.h, location.h) where needed.

After compiling and running, you should be able to type "go east" (or whatever tag you used) to move between your two locations. Try other commands too. Typing "go field" when you are already in the field should give you a message like "You can't get much closer than this."


Conclusion

Congratulations on this significant step forward. You have replaced a rigid, hardcoded map structure with a flexible, data-driven object model. This is a cornerstone of classic game engine design.

Key Takeaways:

  • A unified object model, where all game entities share a common struct, provides immense flexibility.
  • Treating passages as objects allows for descriptive, multi-tagged connections between locations.
  • The distance system is a powerful pattern for centralizing interaction logic, making your code cleaner, more scalable, and easier to debug.
  • Using a switch statement on an enum result (getDistance) is a robust way to handle command execution.

In our next lesson, we will expand on this foundation. We'll implement handlers for displaying player status, inventory, and room descriptions, leveraging the very same getDistance function to intelligently list items that are distHere (in the room) or distHeld (in your inventory).