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.
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:
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.
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.
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:
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";
}
}
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.
@global_append
*then
setEnvironment
if environmentType == 'dev' {
<-> cheat_codes
}
@cheat_codes
*then
hear cheat {
-> cheat
}
hear cheat more {
-> cheat_more
}
>> RETURN
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.
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.
import {
InstructionExtension,
DriverExtension,
InstructionExtensionParameter,
DriverExtensionParameter,
} from "@alexa-games/sfb-f";
export class ISPExtension implements InstructionExtension, 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.
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:
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:
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.
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:
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;
}
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.
@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.
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!