Skill Flow Builder Tips and Tricks: Use Extensions to Level Up Your Narrative-Driven Games

Stef Sharp Oct 09, 2019
Share:
Tips & Tools Tutorial Build Game Skills
Blog_Header_Post_Img

For most experiences, the SFB Editor and features are enough to build a rich experience with dynamic responses that keep users returning. But what happens when you hit the extent of SFB’s base features? Maybe it’s some complex math. Maybe you need to keep track of inventory or divert logic to a mini game. When the plethora of SFB features run out, it’s time to build an extension. Luckily, SFB makes extension building easy.

When to Create an Extension

So you’ve created a robust story-based game using SFB. Your players can travel across the world and fight deadly beasts. They can collect key items for progression and make pivotal plot decisions. At first, the inventory is basic—only a few items to keep track of—but as your story grows, the inventory grows with it. Users may grow frustrated when they’re offered the wrong item at the wrong time. A user who’s in the middle of combat and extremely low on health won’t want to search to find their health potion. They’ll want it to be offered to them without even having to search. Selecting the right composition of items to suggest to a player starts to require more and more conditional statements in SFB’s Editor. This is the point when an extension becomes an asset.

At their core, extensions are simply functions that your SFB story may leverage at any time. If your conditional statements start requiring more than three comparisons or your list of flagged items grows from a manageable 15 to 50, it’s time to look into creating an extension. If it takes 10 lines of logic to do what feels like basic math, it might be time for an extension.

There are three types of extensions: DriverExtension, InstructionExtension, and ImporterExtension. You can learn more about the syntax and functionality of these extension types in the SFB documentation. For the purposes of this blog, we’re going to focus on the extension type you’ll use the most: InstructionExtension.

An InstructionExtension is composed of functions called by the story files as the story is running. Some use cases for the InstructionExtension include:

  1. Complex math on a variable such as exponents and remainder division
  2. Inventory management
  3. Store/catalog management
  4. Iterating over time-dependent components
  5. Mass setting or unsetting variable
  6. Calling external APIs that do not cause the skill session to exit

So what are some ways you might use an InstructionExtension in your own game skills? Let’s dive into some examples. We’ll start with a simple example to get you familiar with the layout of extensions and then move on to a separate advanced example that combines multiple extension types.

Basic Example: Enable/Disable Cheat Codes with Environment Variables

Over time, your SFB story may grow to become a game that can’t be completed in a short amount of time. You may want to be able to easily jump around through the game and automatically set variables as you go. However, you don’t want this functionality to be available to live users. In this example, we’ll use an InstructionExtension to process which version of the skill the player is accessing and then restrict access to content.

To make restricting access easy, we’ll set an environment variable in Lambda with a key of VERSION and possible values of dev or prod. Since this is a variable that is not accessible by SFB automatically, we need to inject that information into the story.

Build the InstructionExtension

When you create a new SFB story, it includes SampleCustomExtension.ts in the code/extensions folder. For ease, we’ll add our environment setter to SampleCustomExtension.ts.

First, replace the code in your SampleCustomExtension.ts file with the following:

Copied to clipboard
import { InstructionExtension, InstructionExtensionParameter } from '@alexa-games/sfb-f';

/**
* Custom InstructionExtension
*/
export class SampleCustomExtension implements InstructionExtension {
    public async setEnvironment(param: InstructionExtensionParameter): Promise<void> {
        console.info("Player environment is: ", process.env.VERSION);
        param.storyState.environmentType = process.env.VERSION ? process.env.VERSION : "dev";
    }
}

Call the Extension from the Story Files

Now that we have an extension, we need to access it from the story files. To prevent production/live skill users from accessing the cheat codes, we can use a simple IF statement to restrict access to a reusable scene called cheat_codes. In this example, if the skill is using the version of “dev” and the user says “cheat,” then it’ll route to the cheat code. Otherwise, the story goes back to the previous scene.

Add the following code to your story.abc file. If you already have @global_append, then you should extend that section with the call to setEnvironment and the environmentType check.

Copied to clipboard
@global_append
    *then
        setEnvironment

if environmentType == 'dev' {
    <-> cheat_codes
}

@cheat_codes
    *then
       hear cheat {
           -> cheat
       }

       hear cheat more {
           -> cheat_more
       }

       >> RETURN

 

Advanced Example: Get User Consumables from the Monetization API

In this example, we’re going to do a simple get request to the Monetization Service Client to determine if a consumable is purchasable. Since monetization is not available in every locale, this allows us to avoid presenting an upsell to users who can’t or shouldn’t be offered the consumable.

Before we get started, make sure you’re familiar with setting up in-skill purchasing (ISP) for a skill and the requirements for consumables. You can read more about in-skill purchasing in the documentation.

Unfortunately, the InstructionExtension can’t access handlerInput and the monetization service requires the user’s locale from handlerInput. However, the DriverExtension can access the request object from Alexa before it reaches the SFB logic. The InstructionExtension allows us to send data back and forth to the story files while the DriverExtension can communicate with external services. Luckily, in SFB you can combine any of the extension types together into a single extension file, so you can use both at the same time.

You can view the full Typescript file for this extension in the Alexa Cookbook code snippets.

Create the File for the Custom Extension

Just like you did in the basic example, you'll need to create a file to hold your extension code. Unlike that example, though, you also need to import DriverExtension and DriverExtensionParameter. Next, to combine two extension types, you just need to implement the additional types in the class. For our ISP extension, you’ll implement InstructionExtension and DriverExtension.
First, add a new file to the extensions folder in your SFB project and name it ISPExtension.ts. Once you have your file ready, add the following code to ISPExtension.ts to create the framework for the extension.

Copied to clipboard
import {
    InstructionExtension,
    DriverExtension,
    InstructionExtensionParameter,
    DriverExtensionParameter,
} from "@alexa-games/sfb-f";

export class ISPExtension implements InstructionExtension, DriverExtension {

}

Build the DriverExtension

The DriverExtension is similar to the request and response interceptors available in the Alexa Skills Kit SDK. The logic is executed before the request reaches SFB and/or before the response is sent to the user. This makes the DriverExtension great for cleaning up data or doing additional logic on story content. A DriverExtension requires both a pre and a post function, but either of these can be left empty. In this case, we only need the pre function to get the handlerInput object.

Copied to clipboard
private handlerInput: any;

async pre(param: DriverExtensionParameter) {
    this.handlerInput = param.userInputHelper.getHandlerInput();

    return;
}

async post(param: DriverExtensionParameter) {
    // Use for post processing, not needed this time
}

Add the following code inside the ISPExtension class you created in the previous step to add pre and post functionality:

Build the InstructionExtension

Now that we have the handlerInput, we can send requests to the Monetization Service Client and also access the user’s locale. The next step is to add two functions: one to check purchasable status and one to check the number of consumables purchased. Additionally, there is a separate function for making the request to the Monetization Service Client.

Purchasable

The sole goal of this extension is to be easily callable from the story files. The function for “purchasable” sets the type of request the skill is making; in this case, the type is “purchasable.” We’ll then use a variable from the storyState, monetizationPurchasable, to flag whether the item is available. storyState is passed back and forth from the story files and contains details about the user such as current point in the story and any variables that have been added or flagged over time.

Once purchasable and request type (workflowType) are set, the function simply triggers a call to the Monetization Service Client via the getMonetizationData function.

Add the following code below the pre and post code you added earlier:

Copied to clipboard
public async purchasable(param: InstructionExtensionParameter): Promise {
    param.instructionParameters.workflowType = "purchasable";
    param.storyState.monetizationPurchasable = false;

    param.storyState = await this.getMonetizationData(param);

    return;
}

Consumable

The function for consumable is intended to retrieve the amount of a consumable that’s been purchased and is available for a user. All this basic function needs to do is set the workflowType of “consumable.”

Add the following code for consumable below the purchasable function. This function just sets the workflowType and allows the consumable checks to be called separately from purchasable checks.

Copied to clipboard
public async consumable(param: InstructionExtensionParameter): Promise {
    param.instructionParameters.workflowType = "consumable";

    param.storyState = await this.getMonetizationData(param);
    return;
}

getMonetizationData()

While purchasable and consumable are vanity calls to make the monetization checks easily referable from the story files, the getMonetizationData function does all of the work for calling the Monetization Service Client. The structure is almost identical to standard Node.js calls to the client, with some added references to storyState for the amount of the consumable that has been purchased.

The following code does additional checks to verify if the consumable amount is out of sync with what is being stored by the skill. Add this section to ISPExtension.ts below the consumable function you added in the previous step:

Copied to clipboard
private async getMonetizationData(
    param: InstructionExtensionParameter
): Promise {
    const product = param.instructionParameters.item; // Supplied from the story file

    if (!product) {
        throw [AlexaMonetizationExtension Syntax Error] monetized item=[${product}] not provided.;
    }

    const ms: any = this.handlerInput.serviceClientFactory.getMonetizationServiceClient();
    const locale: string = this.handlerInput.requestEnvelope.request.locale;

    const isp: any = await ms.getInSkillProducts(locale).then((res: any) => {
        if (res.inSkillProducts.length > 0) {
            let item = res.inSkillProducts.filter(
                (record: any) =>
                 record.referenceName === product
                );
            return item;
        }
    });

    // Return product information based on user request
    if (param.instructionParameters.workflowType === “purchasable”) {
        if (isp && isp[“purchasable”] === “PURCHASABLE”) {
            console.info(“Item is purchasable: “, isp.name);

            // Easily indicate within the story the item is purchasable
            param.storyState.monetizationPurchasable = true;
        } else {
            console.info(“Item cannot be purchased: “, product);
        }
    } else if (param.instructionParameters.workflowType === “consumable”) {
        if (isp && isp.activeEntitlementCount) {
            let itemAmount: number = parseInt(isp.activeEntitlementCount);
            param.storyState[${product}Purchased] = itemAmount;

            // Set the purchased and consumed session variables to keep track during game
            if (itemAmount) {
                if (!param.storyState[${product}Consumed]) {
                    param.storyState[${product}Consumed] = 0;
                }

                if (param.storyState[${product}Consumed] > itemAmount) {
                    // User shows as using more of the consumable than purchased
                    param.storyState[${product}Consumed] = itemAmount;
                }
            }
            
            param.storyState.monetizationPurchasable = true;
        } else {
            console.info(“Item is not available: “, product);
            param.storyState[${product}Consumed] = 0;
            param.storyState[${product}Purchased] = 0;
            param.storyState[${product}] = 0;
            param.storyState.monetizationPurchasable = false;
    }
}

return param.storyState;
}

Call the Extension from the Story Files

We have an extension and we have some basic parameters for checking the state of a consumable. Now let’s call it from a scene in the story. For the sake of this example, we’re making a redundant check if the item is purchasable to demonstrate how each function works. In practice, you can just use the consumable function since it already checks if an ISP item is purchasable.

Add the following code for the reusable @check_item scene to your story.abc file. To test the code, you can follow the Basic example and call @check_item from @global_append.

Copied to clipboard
@check_item
    *then
        purchasable item=’coffee’

        if monetizationPurchasable {
            consumable item=’coffee’

            // Reset the amount of the consumable that is available to use
            set coffee to coffeePurchased
            decrease coffee by coffeeConsumed

            -> has_item_scene
        }

        if !monetizationPurchasable {
            -> no_buy_scene
        }

Now, if you release your skill in a locale that doesn’t support monetization, you can avoid sending users an upsell dialog by first checking if the item is available. You can also keep the amount of a consumable that is available up-to-date as the user progresses through the skill.

This may seem like a complex extension, but at the core all we’ve done is take an API call and add some additional story variables to it.

Conclusion

Extensions are a great tool for passing story variables back and forth without having to do complex SFB logic within the story files themselves. We went through a basic example to access data not readily available to the story files and then a more advanced example of how to call external APIs with SFB. Extensions allow you to add more robust logic to your story-based games and take your adventures from simple narratives to leveling adventures. You can now take this knowledge and add combat modules, character progression, and get those health potions to the right players when they need them.

We’re always excited to hear about your extensions, so feel free to share your creations with us on Twitter!