Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2025/textadventure-behaviour

Text adventures: Behaviours for flavour

Where we can put the code that lets the world react to the player?

Published 15 December 2025
Text Games C# ~4 min. read
This is post 3 in an ongoing series titled Text adventures

Last time up we created the very basics of a text adventure where we could look at rooms in the world, and had a way to provide other commands like movement or picking stuff up. So what can we do to let the world react to the user's actions, and create puzzles and atmosphere for a game? Step forward behaviours...

What is a behaviour? url copied!

If we want to do more interesting things in the game engine, then we need a way to plug in to the process of the player giving commands.

For example, imagine we want to have a locked door. It's not a very scalable approach to put code for "a specific door in our world has a lock" into a command we build for moving. That would get messy fast. But what if we made the move command ask for permission before transitioning the player between rooms? In that scenario you can have something in your world (like the room the player wants to leave or enter) answer "no" to this question if the player doesn't have the right key.

To build this, we can think of most commands having two phases: Asking "can I do the thing?" of all the appropriate entities around the player, and then if the answer was yes making the right change to the game world's state and then telling those same entities that "I have done the thing!". Behaviours are the objects attached to entities to do these checks and generate the right responses.

sequenceDiagram
    participant Command
    participant Entity
    participant Behaviours

    loop For each entity
      Command ->> Entity: Can I do the thing?
      loop For each behaviour
        Entity ->> Behaviours: Query
        Behaviours ->> Entity: Response
      end
      Entity ->> Command: Yes you can!
    end

    loop For each entity
      Command ->> Entity: I've done the thing!
      Entity ->> Behaviours: Notification
    end

					

Like commands, they inherit from that Script base type from the last entry, but they don't need to provide help info, and they return a boolean for:

public abstract class Behaviour : Script
{
    public Behaviour(string name) : base(name)
    {
    }

    public abstract bool Execute(Entity owner, World world, Action action);
}

					

Whenever a command which needs to check in with entities runs it will call Execute() on each behaviour attached to all the relevant items. And this introduces another new thing - the Action type. This is used to represent whatever activity is being processed. These actions potentially get passed to quite a lot of places, so having a simple way to represent them helps with that.

The base type for action stores four entities and a string - to represent any entities and text involved:

public abstract class Action
{
    protected Entity? ParameterOne;
    protected Entity? ParameterTwo;
    protected Entity? ParameterThree;
    protected Entity? ParameterFour;
    protected string? Data;
}

					

But that's not very easy to work with. What are the entities involved in a specific action supposed to be? So it's convenient to make a class for each of the actions which can wrap up those standard parameters with more obvious types and names:

public class CanGetItemAction : Action
{
    public Room Room => (Room)base.ParameterOne!;
    public Item Item => (Item)base.ParameterTwo!;
    public Player Player => (Player)base.ParameterThree!;

    public CanGetItemAction(Room room, Item item, Player player)
    {
        base.ParameterOne = room;
        base.ParameterTwo = item;
        base.ParameterThree = player;
    }
}

					

So this allows code to use something like if(action is CanGetItemAction) to check if this an action we want to process, and then it's easy to extract the relevant entities to process them.

And with these actions in place, a command for "get" can use them to check if getting is allowed. After parsing out the relevant parameters and checking the item is available to be picked up, it can ask assorted entities if the player can get the item. For each of these entity sets, an behaviour that returns false to the "can get" action stops the process of the get command. But if it's not stopped, the item is then moved, and all the same entities are notified with the "has got" action. The result of that second call isn't important, as you can't stop the "get" after it's happened. But they can still output messages if required:

public class GetCommand : Command
{
    public GetCommand() : base("get","get <room item>")
    {
    }

    public override void Execute(World world, string[] parameters)
    {
        if(parameters.Length == 0)
        {
            Console.WriteLine("Get what?");
            return;
        }

        var itemName = parameters[0];

        var item = InputParser.MatchEntities(world.Player.Room.Items, itemName);

        if (item == null)
        {
            Console.WriteLine($"There's no {itemName} here");
            return;
        }

        var canGet = new CanGetItemAction(world.Player.Room, item, world.Player);

        if (VerifyCommand(world.Player.Room, world, canGet) == false)
        {
            return;
        }
        if (VerifyCommand(world.Player, world, canGet) == false)
        {
            return;
        }
        if (VerifyCommand(item, world, canGet) == false)
        {
            return;
        }
        if (VerifyCommand(world.Player.Room.Characters, world, canGet) == false)
        {
            return;
        }

        world.Player.GetItem(item);

        var hasGot = new HasGotItemAction(world.Player.Room, item, world.Player);

        VerifyCommand(world.Player.Room, world, hasGot);
        VerifyCommand(world.Player, world, hasGot);
        VerifyCommand(item, world, hasGot);
        VerifyCommand(world.Player.Room.Characters, world, hasGot);

        Console.WriteLine($"You get {item.Article} {item.Name}");
    }
}

					

So going back to the simple example from the last post, we had an item for a seagull. How can we stop the player picking up that gull? Something like this, attached to the gull:

public class CantGetBehaviour : Behaviour
{
    public CantGetBehaviour() : base("cantget")
    {
    }

    public override bool Execute(Entity owner, World world, Action action)
    {
        if (action is CanGetItemAction cgi)
        {
            Console.WriteLine($"You can't pick up {cgi.Item.Article} {cgi.Item.Name}.");
            return false;
        }

        return true;
    }
}

					

This is a fairly generic approach - if the behaviour receives a CanGetItemAction it denies it with a simple "you can't get that" message. But you're free to make more specific things if your game idea requires them.

These behaviours need registering with both the world and the entity that's using them:

world.RegisterBehaviour<CantGetBehaviour>();

var gull = world
    .CreateItem("A", "gull", "It's a black-headed gull. It is very noisy, and it is clearly scanning the area looking for chips to steal.")
    .AddBehaviour("cantget");

					

So now if you try to pick up the gull, it will refuse:

> look

The beach
A wide sandy beach. The waves lap gently to the south, and a path winds off through the sand dunes to the north. Seagulls squawk noisily above you.
Exits:
  To the south is The sea
Characters:
Items:
  A gull
> get gull

You can't pick up A gull.
>

					

This approach is very flexible. You can make locked doors, vending machines, conversations with NPCs, encumberance systems or combat with an approach like this. And the key improvement over hard-coding these sorts of behaviours this into the engine is that it allows implementing things in a way that matches the story, rather than one which matches the engine.

As before, the example code for all the stuff coming in this series, and an example game is available on Github.

Next up: Bringing all this together into the full example to close off the series.

↑ Back to top