D20 RPG – Item Pickup

You’ve probably noticed I have left a few placeholder spots for entry “links” along the way. In this lesson we will implement one by adding a sort of item inventory.

Overview

In the Explore lesson, you got a glimpse at one of the Entry screen features. In particular some of the main body’s text had color, and I mentioned that it would be interactive text. The TextMeshPro Text (UI) component has the ability to render HTML tags, and to handle things like determining where in the rendered text a user has clicked.

Take a look at the Assets -> Objects -> EntryStyleSheet.asset and you may observe how one or more HTML tags can be given a “name” and then used as a “style” within the entry text. In this lesson, we will use the “style” named “pickup” to show that there is an item that can be picked up. We will even let the item play a part in the way the story unfolds.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Adventure Item System

Create a new C# script at Assets -> Scripts -> Component named AdventureItemSystem and add the following:

public enum AdventureItem
{
    SkeletonKey,
    Torch
}

An enum is a very simple way to represent something. Under the hood, it is basically just a named number. A more complete game may want something more complex, but you could always use these to load assets by the same name – something like the way we handle the Entry.

Add the following:

public partial class Data
{
    public CoreSet<AdventureItem> items = new CoreSet<AdventureItem>();
}

For now, all I need is a simple “inventory” that keeps track of what a user has or doesn’t have. Order doesn’t really matter, so a simple “Set” of the items a player holds can be tracked in the Game Data using the above “items” collection.

Add the following:

public interface IAdventureItemSystem : IDependency<IAdventureItemSystem>
{
    void Take(AdventureItem item);
    void Drop(AdventureItem item);
    bool Has(AdventureItem item);
}

Here I have added an injectable interface for a system that works with our AdventureItem. The “Take” method would let you pickup an item – and add it to the “items” inventory. The “Drop” method will remove an item from the inventory. Finally, the “Has” method will let us know if the player has a type of item or not.

Add the following:

public class AdventureItemSystem : IAdventureItemSystem
{
    CoreSet<AdventureItem> Items { get { return IDataSystem.Resolve().Data.items; } }

    public void Take(AdventureItem item)
    {
        Items.Add(item);
    }

    public void Drop(AdventureItem item)
    {
        Items.Remove(item);
    }

    public bool Has(AdventureItem item)
    {
        return Items.Contains(item);
    }
}

This class implements our interface and is hopefully pretty clear.

Injection

Next, open the Assets -> Scripts -> Component -> ComponentInjector and add the following line to the Inject method:

IAdventureItemSystem.Register(new AdventureItemSystem());

This will make sure that our system will be instantiated and ready when we need it.

Alert

Once a user interacts with an Entry Link, we will need some way to inform the user that an action has occurred. For this, we will use a simple Alert that pops up on screen with a message. Open the script Assets -> Scripts -> UI -> Alert.

Add the following to the IAlert interface:

UniTask Show(string message);

This provides a way for us to show a message to a user, and because it is an interface we don’t need to know how that happens.

Add the following to the Alert class:

public async UniTask Show(string message)
{
    // Configure and show the alert
    label.text = message;
    await rootPanel.ScaleTo(Vector3.one, 0.25f, EasingEquations.EaseOutBack).Play();

    // Wait for user to click Ok
    using (var handler = button.GetAsyncClickEventHandler(this.GetCancellationTokenOnDestroy()))
        await handler.OnClickAsync();

    // dismiss the alert
    await rootPanel.ScaleTo(Vector3.zero, 0.25f, EasingEquations.EaseInBack).Play();
}

When we show an alert, there will be a UI panel that appears with an animated transition where it scales from zero to its full size. The user can read the message and then click an “Ok” button, at which point the alert will transition out by scaling back down to zero.

Add the following to the Alert class:

private void OnEnable()
{
    IAlert.Register(this);
    rootPanel.localScale = Vector3.zero;
}

private void OnDisable()
{
    IAlert.Reset();
}

Here we are using the OnEnable method to register our UI panel with the IAlert interface and to set the initial scale of the alert to zero. The OnDisable method will cleanup the interface registration.

Open the “Explore” scene. Find and select the “Alert” object in the root of the Hierarchy pane. In the Inspector, toggle the checkbox to activate the game object. It will be visible in the scene, but in the game, it will hide itself until we need it. Make sure to save your changes.

Pickup Entry Link

The next script we will make will be something that can be attached to Entry objects, and will know how to respond to text that a user might interact with. Similar to how I provided an interface for the Entry “Options”, let’s add an interface for these “Links”. Add a new C# script to Assets -> Scripts -> SoloAdventure -> Explore named IEntryLink and add the following:

using Cysharp.Threading.Tasks;

public interface IEntryLink
{
    UniTask Select(string link);
}

When interacting with an Entry’s “Link”, something will happen. It could be immediate, or it could trigger a whole new flow of code. Therefore, the interface returns a UniTask, so that we can “await” the completion as needed.

Add a new C# script to Assets -> Scripts -> SoloAdventure -> Explore named PickupEntryLink and add the following:

using UnityEngine;
using Cysharp.Threading.Tasks;

public class PickupEntryLink : MonoBehaviour, IEntryLink
{
    [SerializeField] AdventureItem item;
    [SerializeField] string takeMessage;
    [SerializeField] string dropMessage;

    public async UniTask Select(string link)
    {
        if (link != "pickup")
            return;

        var system = IAdventureItemSystem.Resolve();
        if (!system.Has(item))
        {
            system.Take(item);
            await IAlert.Resolve().Show(takeMessage);
        }
        else
        {
            system.Drop(item);
            await IAlert.Resolve().Show(dropMessage);
        }
    }
}

The component has a few serialized fields. One is an “item” that can be picked up. Next, I have provided fields as placeholder messages, so you can customize the message that appears in the alert when a player either takes or drops the item.

The “Select” method is pretty simple. It first verifies that the “link” that we are interacting with is in fact a type of “pickup” link, and if not, it will abort. This might be useful in case there are other types of interactions that could also occur within an Entry.

Next, the “Select” method will grab a reference to our new IAdventureItemSystem to determine whether or not the player already has the item. If the player doesn’t have the item, then we will “Take” it, otherwise we will “Drop” it. Regardless of the path chosen, we also await the Alert being displayed with an appropriate message based on the action that was taken.

Item Based Exploration

Our current Entry “Option” is super basic – it merely specifies a fixed “Entry” name which should be the next one to appear. Now we can make something a little more dynamic. We will make an “Option” that routes differently depending on whether or not we have a specified item.

Create a new C# script at Assets -> Scripts -> SoloAdventure -> Explore -> EntryOptions named ItemExploreEntryOption and add the following:

using UnityEngine;

public class ItemExploreEntryOption : MonoBehaviour, IEntryOption
{
    [SerializeField] string text;
    [SerializeField] AdventureItem item;
    [SerializeField] string hasItemEntry;
    [SerializeField] string noItemEntry;

    public string Text
    {
        get { return text; }
    }

    public void Select()
    {
        var entry = IAdventureItemSystem.Resolve().Has(item) ? hasItemEntry : noItemEntry;
        IEntrySystem.Resolve().SetName(entry);
    }
}

There are several serialized fields:

  • text – is the label to show on the button for the option
  • item – is the adventure item to base the navigation requirement on
  • hasItemEntry – is the name of an entry asset to navigate to if the user has the item
  • noItemEntry – is the name of an entry asset to navigate to if the user does not have the item

The Text property provides read-only access to the “text” field, and is part of conformance of the IEntryOption interface.

The Select method is also part of the IEntryOption interface – and is what is called whenever the option has been selected by the user. In this case, it will coordinate between the IAdventureItemSystem and the IEntrySystem so that it routes the story based on whether or not the user has the specified item.

Entry

Open the C# script, Assets -> Scripts -> SoloAdventure -> Explore -> Entry. At the top of the script, add another “using” statement:

using Cysharp.Threading.Tasks;

To the IEntry interface, add the following:

UniTask SelectLink(string link);

To the Entry class, add the following:

public async UniTask SelectLink(string link)
{
    await GetComponent<IEntryLink>().Select(link);
}

Entry Flow

Open Assets -> Scripts -> Flow -> EntryFlow and locate the “TODO” comment:

// TODO: Selected a link in the text

Replace the TODO with the following:

// Selected a link in the text
await entry.SelectLink(link);

Create Entries

Let’s edit and create some entries so that we can try out the new game mechanic.

Entry_02

Our “Entry_02” is supposed to have options to “Fight” or “Run Away”, but since we haven’t implemented the “Encounter” phase of our game yet, it only shows the “Run Away” option. For now, let’s create another placeholder option on that Entry.

Add another ExploreEntryOption where the “Text” is “Fight” and the “Entry Name” is “Entry_05”. Note that I intentionally skipped “Entry_04” because I will need another entry to hold story for when someone loses the fight.

Entry_05

Create a new Prefab asset called “Entry_05” in the same location as our other Entry assets. Make sure that “Addressable” is checked on.

Add an Entry component. For the Text I used:

You’ve emerged victorious. Well done! Now that you have time to look around you notice a *<style=”pickup”>key</style> on the ground.
 *Note, you can click the colored word to interact with the item it mentions.

Notice that I wrapped the word “key” in a “style” tag with the name “pickup” – this is what causes the word to be treated as a red link. Remember that you can see what the pickup style means by looking in the EntryStyleSheet.asset.

Add a PickupEntryLink component. Configure the component with the following:

  • Item: Skeleton Key
  • Take Message: You take the skeleton key
  • Drop Message: You drop the skeleton key

Add a ItemExploreEntryOption component. Configure the component with the following:

  • Text: Next
  • Item: Skeleton Key
  • Has Item Entry: Entry_06
  • No Item Entry: Entry_07

In order to verify that our navigation is working, we will also need to implement the new destination Entry assets…

Entry_06

Create a new Prefab asset called “Entry_06” in the same location as our other Entry assets. Make sure that “Addressable” is checked on.

Add an Entry component. For the Text I used:

As you continue your adventure you come upon a locked door. Good thing you took that key from before! You unlock the door successfully and continue inside.
 It isn’t long before you come across a gaping hole. You might be able to jump across.

The option for this will eventually be dynamic and based on a skill check. For now, we can use a placeholder ExploreEntryOption where the “Text” is “Jump” and the “Entry Name” is left blank.

Entry_07

Create a new Prefab asset called “Entry_07” in the same location as our other Entry assets. Make sure that “Addressable” is checked on.

Add an Entry component. For the Text I used:

As you continue your adventure you come upon a locked door. If only you had taken a key! I guess there is nothing left to do but go home.

Then add an ExploreEntryOption where the “Text” is “Go Home” and the “Entry Name” is left blank.

Try It Out!

Save your project then head over to the “LoadingScreen” Scene. Press Play and try out the new and improved adventure. When you reach the scene with the skeleton key, try playing where you pick it up. Then try once where you don’t pick it up, or where you pick it up then drop it by clicking the link a second time. See how the story changes based on whether or not you have the key.

Summary

In this lesson we implemented everything necessary to have operational “links” in our entry text. We used the link as an opportunity to pickup an adventure item, like a skeleton key, and then let the item serve as a condition for story advancement.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *