How It Works

The Canisters

There are three types of canisters in the World Engine:

  1. World canister: The game server canister

  2. World Hub canister: The indexing canister

  3. User Node canisters: The user data canisters

World canister

A World canister is essentially your game server written as a canister smart contract. You can deploy your own through our website. Your game client communicates exclusively with this canister. Its main purpose is:

  • Act as the game server for your game

  • Manage the Configs of your game

  • Validate the actions that players are doing in your game

  • Ensure players aren’t cheating

World Hub

The World Hub indexes World canisters and User Node canisters. It tells the World canisters which User Node canister they need to call.

User Node

UserNode canisters store the data of all users. User data is stored as pieces of data called Entities.


The Data

There are four types of data stored across the World, World Hub, and User Nodes:

  1. Action: Actions enforce what players can do in your World

  2. Config: Configs define metadata, usually about Entities

  3. Entity: Entities are data representing the objects or things in a World

  4. Permissions: Defines which Worlds have permission to alter Entities in your World

Action

An Action is something a player can do in your game that gets saved to the database.

Example: If a player spends 100 Gold they receive 1 Sword

Action

This is what an Action looks like in code:

    public type Action = 
    {
        aid : Text;
        callerAction : ?SubAction;
        targetAction : ?SubAction;
        worldAction : ?SubAction;
    };
  • aid: The action id that identifies this specific Action

  • callerAction: The SubAction to apply to the user calling the Action

  • targetAction: The SubAction to apply to the user being targeted by the Action

  • worldAction: The SubAction to apply to the World executing the Action

SubAction

This is what a SubAction looks like in code:

    public type SubAction =
    {
        actionConstraint : ?ActionConstraint;
        actionResult : ActionResult;
    };
  • actionConstraint: The requirements for a player to do this Action

  • actionResult: The result of this Action

Config

A Config defines static metadata for your game. Configs are stored in the World canister, and are fetched by the Unity game client when the game starts. They can be changed in the canister without requiring a new game build. They look like this:

public type Config = {
    cid : Text;
    fields : Map.Map<Text, Text>;
};
  • cid: The config id for this Config

  • fields: A hashmap of "fields" and values. This allows for flexibility to define whatever fields you want to store in this Config.

Entity

In its simplest definition, an Entity is a piece of data in the database. They look like this:

    public type Entity = {
        wid : TGlobal.worldId;
        eid : TGlobal.entityId;
        fields : Map.Map<Text, Text>;
    };
  • wid: The world id in which this entity exists (aka the canister id of the World canister)

  • eid: The entity id that uniquely identifies this Entity (eg. awesome_item_01)

  • fields: A hashmap of fields and values contained in the Entity

This standardized data format for Entities can represent almost any data type imaginable.

The most common use cases for Entities include Items, Buffs, and Stats. But they can be used for more complex data like Characters, User Profiles, Purchases, Tech Tree etc.

Entities are the building blocks of your game economy.

Here are some example implementations:

Item Entity:

{
 eid="item_01"                       //entity id
 wid="awcae-maaaa-aaaam-abmyq-cai"   //world id (aka the canister id of your world)
 fields = [
  ("quantity", "420"),             //quantity of item user holds
 ];
};

Buff Entity:

{
 eid="buff_01"
 wid="awcae-maaaa-aaaam-abmyq-cai"
 fields = [
  ("expiration", "1695954265797"),            // expiration timestamp
 ];
};

Stat Entity:

{
 eid="stat_01"
 wid="awcae-maaaa-aaaam-abmyq-cai"
 fields = [
  ("games_played", "420"),            
 ];
};

Character Entity:

{
 eid="warrior_01"
 wid="awcae-maaaa-aaaam-abmyq-cai"
 fields = [
  ("attack", "420"),
  ("defense", "69"),
  ("health", "9000"),            
 ];
};
Permissions

Permissions allow other games to alter your game's database.

There are two types of permissions:

GlobalPermission allows another game World to change all the Entities in your World.

public type GlobalPermission = {
    wid : Text; // The World Id of the World that you're giving permission to
};

EntityPermission allows another game World to change a specific Entity in your World.

public type EntityPermission = {
    wid : Text; // World Id of the World that you're giving permission to
    gid : Text; // Group Id of the Entity
    eid : Text; // Entity Id of the Entity
};

These are the functions in your World canister that allow you to add and remove permissions:

public shared ({ caller }) func grantEntityPermission(permission : EntityPermission) : async () {
    assert (isAdmin_(caller));
    await worldHub.grantEntityPermission(permission);
};

public shared ({ caller }) func removeEntityPermission(permission : EntityPermission) : async () {
    assert (isAdmin_(caller));
    await worldHub.removeEntityPermission(permission);
};

public shared ({ caller }) func grantGlobalPermission(permission : GlobalPermission) : async () {
    assert (isAdmin_(caller));
    await worldHub.grantGlobalPermission(permission);
};

public shared ({ caller }) func removeGlobalPermission(permission : GlobalPermission) : async () {
    assert (isAdmin_(caller));
    await worldHub.removeGlobalPermission(permission);
};


Below is a diagram of how game World canisters interact with the World Hub and User Node canisters:

Last updated