Topia offers a robust SDK and API that allows anyone to build custom apps or integrate any 3rd-party content, application, game or learning experience. Dedicated deployments launch with dozens of SDK Applications, including mini-games and social apps made by the Topia team and our developer community
Questions, comments, or have something exciting to share with the Topia team? Reach out to apps@topia.io!
Visitors of a topia.io world interact with each other and the interactively configured assets in your world without the need for an API Key. This is all made possible through Interactive Session credentials passed to the SDK with every request, when applicable. What does this mean for you? Not much, actually! All of the magic happens behind the scenes and all you have to do is make sure that new class constructors include an options object like this: options: WorldOptionalInterface = { attributes: {}, credentials: {} }
and all calls to this.topia.axios
include the inherited this.requestOptions
parameter.
Getting a Key Pair:
Adding a Key Pair to an asset:
Make an asset “interactive” by adding your PUBLIC key to the integrations page of the asset editor.
await DroppedAsset.get(assetId, urlSlug, {
credentials: {
interactivePublicKey,
interactiveNonce,
visitorId,
},
});
A Topia provided API Key can be included with every object initialization as a parameter named apiKey
. This API Key is used to in authorization headers in all calls to the Public API. Use it wisely and sparingly! The API Key should only be used to authorize your app (and therefore all of it's users) to perform specific actions when not currently in a Topia world. In most case the ability to interact with an SDK application should be controlled per user using Interactive Credentials (see above).
We have two boilerplates available to help you get started. Pick the one that best suits your needs, clone it, and let the coding begin!
Run yarn add @rtsdk/topia
or npm install @rtsdk/topia
Create your instance of Topia and instantiate the factories you need:
dotenv.config();
import dotenv from "dotenv";
import { AssetFactory, Topia, DroppedAssetFactory, UserFactory, WorldFactory } from "@rtsdk/topia";
const config = {
apiDomain: process.env.INSTANCE_DOMAIN || "https://api.topia.io/",
interactiveKey: process.env.INTERACTIVE_KEY,
interactiveSecret: process.env.INTERACTIVE_SECRET,
};
const myTopiaInstance = new Topia(config);
const Asset = new AssetFactory(myTopiaInstance);
const DroppedAsset = new DroppedAssetFactory(myTopiaInstance);
const User = new UserFactory(myTopiaInstance);
const World = new WorldFactory(myTopiaInstance);
export { Asset, DroppedAsset, myTopiaInstance, User, World };
Put it to use:
import { DroppedAsset } from "./pathToAboveCode";
export const getAssetAndDataObject = async (req) => {
const { assetId, interactiveNonce, interactivePublicKey, urlSlug, visitorId } = req.query;
const droppedAsset = await DroppedAsset.get(assetId, urlSlug, {
credentials: {
interactiveNonce,
interactivePublicKey,
visitorId,
},
});
await droppedAsset.fetchDroppedAssetDataObject();
return droppedAsset;
};
Data Objects can be used to store information such as game state, configurations, themes, and analytics. There are three types of Data Objects:
await world.updateDataObject({
[`keyAssets.${keyAssetId}.itemsCollectedByUser.${profileId}`]: { [dateKey]: { count: 1 }, total: 1 },
[`profileMapper.${profileId}`]: username,
});
await world.incrementDataObjectValue([`keyAssets.${keyAssetId}.totalItemsCollected.count`], 1);
await droppedAsset.setDataObject(
{
...defaultGameData,
keyAssetId: droppedAsset.id,
},
{ lock: { lockId, releaseLock: true } },
);
await droppedAsset.updateDataObject({ lastInteraction: new Date(), playerCount: playerCount + 1 });
js await world.incrementDataObjectValue([`totalMessagesSentCount`], 1);
All of our data object set, update, and increment methods have an optional lock argument. You can create a lock id using that parameters specific to the action you are taking plus a timestamp so that the lock will expire after a certain amount of time has passed. As an example, TicTacToe allows users to Reset the game board so that they can start a new game but we'd only want the reset to happen once even if the user(s) press the button multiple times. To prevent multiple resets from happening within a 10 second window (stopping the calls from going through and preventing the race condition), we'd lock the object by doing the following:
try {
await droppedAsset.updateDataObject(
{ isResetInProgress: true },
{
lock: { lockId: `${assetId}-${resetCount}-${new Date(Math.round(new Date().getTime() / 10000) * 10000)}` },
},
);
} catch (error) {
return res.status(409).json({ message: "Reset already in progress." });
}
Using the code above would also allow us to check if isResetInProgress === true
and prevent the code from progress immediately. Make sure that when using a lock such as this that you also call await droppedAsset.updateDataObject({ isResetInProgress: false })};
after all reset functionality has been executed.
Turn based locking Locking data object updates can also be extremely helpful when building a turn based game. As an example, TicTacToe should only allow one user to take a turn at a time. To prevent multiple moves at once we could use the following:
try {
const timestamp = new Date(Math.round(new Date().getTime() / 5000) * 5000);
const lockId = `${keyAssetId}-${resetCount}-${turnCount}-${timestamp}`;
await droppedAsset.updateDataObject({}, { lock: { lockId, releaseLock: false } });
} catch (error) {
return res.status(409).json({ message: "Move already in progress." });
}
Once complete be sure to also call await keyAsset.updateDataObject({ turnCount: turnCount + 1 });
so that the next player is free to take their turn!
Custom analytics
You can leverage the data object methods for all types to track analytics unique to your Public Key by passing analytics
as an optional array along with profileId
, urlSlug
, and/or uniqueKey
to all calls that set, update, or increment data objects!
World and Dropped Asset classes will automatically include urlSlug
. In addition to analytics
you can also pass profileId
if you want to track event per user and/or a uniqueKey
to additionally track uniqueness of the event for all time, per user (if profileId
is included), and per world.
Examples leveraging World data objects calls:
await world.setDataObject({ hello: "world" }, { analytics: [{ analyticName: "resets"} ], lock: { lockId, releaseLock: true });
await world.updateDataObject({}, { analytics: [ {analyticName: "matches", uniqueKey: `${playerOneProfileId}-${playerTwoProfileId}`, urlSlug }], });
await world.incrementDataObjectValue(`keyAssets.${assetId}.completions`, 1, { analytics: [{ analyticName:"completions", incrementBy: 2, profileId, uniqueKey: profileId, urlSlug }] });
Visitor and User classes will automatically include profileId
. In addition to analytics
you can also pass urlSlug
if you want to track event per world and/or a uniqueKey
to additionally track uniqueness of the event for all time, per user, and per world (if urlSlug
is included).
Examples leveraging Visitor data objects calls:
await visitor.setDataObject(
{ hello: "world" },
{ analytics: [{ analyticName: "starts" }], lock: { lockId, releaseLock: true } },
);
await visitor.updateDataObject(
{},
{ analytics: [{ analyticName: "emotesUnlocked", profileId, uniqueKey: profileId }] },
);
await visitor.incrementDataObjectValue(`completions`, 1, {
analytics: [{ analyticName: "completions", incrementBy: 2, profileId, urlSlug, uniqueKey: profileId }],
});
Note: This does NOT impact the data objects themselves but rather allows you to track custom analytics (incremented by 1) across all instances of your application with a given Public Key.
Run gh repo clone metaversecloud-com/mc-sdk-js
We've added an Issue template to help standardize Issues and ensure they have enough detail for a developer to start work and help prevent contributors from forgetting to add an important piece of information.
We've added a Pull Request template to help make it easier for developers to clarify what the proposed changes will do. This helps facilitate clear communication between all contributors of the SDK and ensures that we are all on the same page!
The SDK Stylesheet is already added to every boilerplate. To view documentation and examples please click here.
We use TypeDoc to convert comments in TypeScript source code into rendered HTML documentation. Comments should be simple and concise and include examples where applicable. Please be sure to add or update comments accordingly!
To update docs run yarn docs
.
To view docs locally open mc-sdk-js/clients/client-topia/docs/modules.html
in your browser.
Example of Class comments:
/**
* @summary
* Create an instance of Dropped Asset class with a given dropped asset id, url slug, and optional attributes and session credentials.
*
* @usage
* ```ts
* await new DroppedAsset(topia, "1giFZb0sQ3X27L7uGyQX", "example", { attributes: { text: "" }, credentials: { assetId: "1giFZb0sQ3X27L7uGyQX" } } });
* ```
*/
Example of method comments
/**
* @summary
* Sets the data object for a dropped asset.
*
* Optionally, a lock can be provided with this request to ensure only one update happens at a time between all updates that share the same lock id
*
* @usage
* ```ts
* await droppedAsset.setDroppedAssetDataObject({
* "exampleKey": "exampleValue",
* });
* const { dataObject } = droppedAsset;
* ```
*/
We use Jest for testing and take advantage of dependency injection to pass mock data into our services.
To run the test suite, please run yarn test
.