Scripting 101: How to get started

This tutorial is aimed at people who have coded before but don’t feel comfortable yet to dive straight into the API. It’s good to have some JavaScript knowledge, but not required.

Where do I put my script files?

Scripts have to be created outside of the Tabletop Playground Editor with your favourite editor. We recommend Visual Studio Code, a lightweight editor with code completion for all platforms. After you have created a new package in Tabletop Playground, simply navigate to your package folder and put your script files into the Scripts folder:

[Tabletop Playground Folder]\TabletopPlayground\PersistentDownloadDir\[Name of your package]\Scripts

Any .js script files in this folder can be selected in Tabletop Playground. You can also go to the package folder by clicking the folder icon in the top left when you have your package opened in the editor.

Global vs. object scripts

There are two way to include script files in your project. One global script and objects scripts which are directly associated with individual objects. Note that the following controls and the scripting console are by default only available when starting a preview from the editor. In order to use them in a regular game, go to the Settings, click “Game” and then “Show advanced interface settigns”. Then activate the first option “Scripting UI in Game”.

Global script

The global script can be added through the session options when editing a state for your project.

You can find the session options in the right click menu

It is executed once, as soon as the corresponding state is loaded. It is loaded after all objects have been spawned, so it’s a good place to set up all your game related variables and logic.

Object scripts

An object script is directly attached to an object. It can be added either through a template or directly attached to an individual model when editing a state.

When an object is loaded into the world, any attached object script is executed (once). This happens after everything in the world is loaded (so all objects are there and after the global script has been run. If there is both a directly attached script as well as a template script, only the directly attached script gets executed.

Making stuff happen

As mentioned above, scripts are only executed once. So what you do in your script file is to set up any data and methods that you need and then tell Tabletop Playground when to call which of your functions (registering callbacks).

Don’t put any infinite loops inside the script! This will cause the script to hang and potentially crash Tabletop Playground.

For example for an object you can register an onGrab function which gets called by Tabletop Playground, whenever that object gets grabbed. Let’s look at some example code:

// include the api
const { refObject } = require('@tabletop-playground/api');


// add a new function to be called by Tabletop Playground when
// this object gets grabbed by any player
refObject.onGrab.add(
    function(obj, player)
    {
        console.log("I got grabbed by", player.getName());
    }
);

You can also define the function separately, but you won’t get auto completion for parameters this way. It can help making your code more readable though if you’re writing a long complicated function, or if you want to use the same function in multiple places.

// include the api
const { refObject } = require('@tabletop-playground/api');

// define our function
grabCallBack = function(obj, player)
{
    console.log("I got grabbed by", player.getName());
}

// add a new function to be called by Tabletop Playground when
// this object gets grabbed by any player
refObject.onGrab.add(grabCallBack);

If you add this script file to an object, whenever any player picks it up, it will put the defined message into the script console:

Console output after the chess piece got grabbed by the player Zorfmorf

You probably already noticed the usage of the variable refObject. It’s provided by Tabletop Playground and is the GameObject representation of your object, in the above example the chess piece. We can therefore make use of all the functionality outlined in the API. It’s only available when you attach a script directly to an object.

If you look at the definition of the onGrab function that we used to register our function, the function takes a GameObject (the object being grabbed) and a Player (the player grabbing the object) as parameter, which is why we have to add these parameters to our callback function grabCallBack.

So as a next step, let’s try to add a callback for onTick, which is called on every game tick, many times per second. Be careful what you do inside this method. If you put any complicated computations or queries in here, this can have a bad performance impact!

// include the api
const { refObject } = require('@tabletop-playground/api');

// add a callback for on-tick
refObject.onTick.add(function(object, deltaTime) {
    // let's launch the object slowly upwards like a rocket
    object.applyForce(new Vector(0, 0, 1000));
});

This should launch whatever piece you attach this to slowly upwards. Note that we omitted the second parameter for applyForce (useMass) since it defaults to false which is what we wanted in this situation. If your callback doesn’t seem to work, make sure that you provided all the required parameters.

Global Events

Next we are going to take a look the global script. In generally you want to put all code and callbacks here that is independent of individual objects. Let’s add some global events and try them out!

const { globalEvents } = require('@tabletop-playground/api');

// called whenever a chat message is sent
globalEvents.onChatMessage.add(function(player, message) {
    // show a message on screen of the player who wrote the message
    player.showMessage("Should you really say something like \"" + message + "\"");
});

// called whenever one ore more dice are rolled and sets all dice to six
globalEvents.onDiceRolled.add(function(player, dice) {
    dice.forEach(
        function(d, index) {
            // set the dice to 6 (faces 0 to 5)
            d.setCurrentFace(5);
        }
    );
});
It just works!

The world object

The world object allows you to interact with the world, which is a representation of the currently loaded level and everything that is included in it. It’s especially useful to find all or specific objects and players:

  • Load in new objects
  • Get a reference for a specific objects or a list of all existing objects
  • Draw debug lines
  • Line traces or sphere traces

Scripting basics

You can enhance your packages by programming in JavaScript. There are multiple ways of using JavaScript in TabletopPlayground: you can attach scripts to in-game objects, object templates, or globally to a game state. You can also use a JavaScript console while creating states in the editor, or even while hosting a game!

This article contains basic information about how scripting works and what tools you can use. If you’d rather jump right in, you can start with the tutorial at https://tabletop-playground.com/knowledge-base/scripting-101-how-to-get-started/

An example scripting project can be found at https://tabletop-playground.com/knowledge-base/chess-ai-scripting-example/.

If you already have scripting experience using Lua, one of our community members has written a guide to help you get up to speed with JavaScript in Tabletop Playground quickly: https://steamcommunity.com/sharedfiles/filedetails/?id=2092593343

Writing scripts

Your scripts need to be located in the “Scripts” subfolder of your package (find your packages in your in your install directory under TabletopPlayground/PersistentDownloadDirs, or click the folder icon in the editor window for your package to open the “Templates” subfolder of the package). To interact with Tabletop Playground in your scripts, you need to import the API. For example, you can import the whole API by writing const tp = require('@tabletop-playground/api');. Alternatively, you can directly import what you need, for example const { world, globalEvents } = require('@tabletop-playground/api');. In both cases, our recommended script editor Visual Studio Code (VS Code) will pick up the API definition and provide autocompletion and show documentation in tooltips. You can find the online API documentation at https://api.tabletop-playground.com.

The API has a few variables that you can use to interact with the game: The world object has many methods for finding players and objects, including 3D queries for objects using various shapes (lines, boxes, spheres, etc.). It also has methods to draw points and lines in the world to help visualize and debug your scripts.

Then there’s globalEvents. It contains various hooks to which you can attach callback functions to trigger behavior when certain events happen in the game. For example, the following code will print a message to the JavaScript console whenever a player leaves the game: globalEvents.onPlayerLeft.add(function(player) {console.log("Didn't like " + player.getName() + " anyway");});

The last type of variable is not available in the global script, or in the console. It can only be used in scripts attached to objects in game or object templates: refObject is the referenced object. It has lots of functions to manipulate the object or get information about it. Usually, you will want your object specific code to run when something happens with the object, so refObject also provides callback hooks. These callbacks always get the referenced object as first parameter. For example, the following code prints who released an object where, whenever any player releases it: refObject.onReleased.add(function(object, player) {console.log(player.getName() + " released at " + object.getPosition());});

In addition to refObject, there are some more specific variables that are only available in scripts for certain types of objects: refCard for cards, refHolder for card holders, refContainer for containers, refDice for dice, and refMultistate for multistate objects. Each object type has additional functions and callback hooks, but refObject is always valid. You can find a TypeScript file that defines the complete API in the game folder under TabletopPlayground/node_modules/@tabletop-playground/api/index.d.ts.

Using scripting

The scripting UI elements (including the console) are active when you create game states in the editor. You can also use them in a regular game when you go to the Settings, click “Game” and then “Show advanced interface settigns”. Then activate the first option “Scripting UI in Game”.

You can attach a global script to the game state in the session options, or to every object in their object properties. Objects also have a unique ID that you can find in their properties. You can use the id to get the object in scripts or the console with world.getObjectById.

When editing an object template in the editor, you can also attach a script in the object properties pane. The script will be used by default for all objects of that type, but it can be overridden by specifying a script for a particular object when creating a state. Objects in existing states are also not affected by changing the script of their object template.

While you are editing and testing your scripts, you’ll often want to reload them quickly in game. When you are editing game states, there’s an additional entry in the context menu: “Reload Scripting” will discard and re-initialize the JavaScript state and then execute all scripts, so scripts are executed in a clean environment just like when loading a game state.

Script Execution Order

When loading a state, all Objects are created, then the global script is run, then all Object scripts are run once and all GameObject.onCreated and GlobalScriptingEvents.onObjectCreated events are executed.

Script initialization and callback execution is always run sequentially. You don’t need to worry about any race conditions.

Variable Scope in Object scripts

Note that the global scope is shared between all scripts and the in-game console. For example, any variable you globally define in an object script is shared between all objects running the same script. If you want to define a variable only for a specific object instance, you can instead assign it to the reference Object:

const { refObject } = require('@tabletop-playground/api');

// shared by all game objects running this object script
let someValue = 1; 

// private value for each game object running this script
refObject.someValue = 1;

Debugging and external consoles

When your scripts get more complex, you may want a way to step through your code in order to find errors. Or maybe you are used to the feature-packed JavaScript consoles of the Chrome Browser or VS Code? No problem! Tabletop Playground supports the V8 inspector protocol to connect external applications.

First, you have to execute world.startDebugMode(). You can run the command from the console or from a script, but remember that you should remove it from your scripts before you upload your package. Once the inspector protocol is running, you can connect the Chrome Dev Tools by going to the following URL in Chrome: devtools://devtools/bundled/inspector.html?v8only=true&ws=localhost:9229. You can now use the Chrome Dev Tools console in addition to the in-game console! Both have access to the same JavaScript context.

If you want to use the VS Code console or debugger, you have to configure the connection: go to the Debugger (bug icon in on the left), then open your launch.json by clicking on the small gear on the upper left. Paste the following configuration:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Inspector",
            "type": "node",
            "protocol": "inspector",
            "request": "attach",
            "address": "localhost",
            "port": 9229
        }
    ]
}

Save the new config, and press “Start debugging” (the green arrow next to the config button). The debug console from VS Code should now be connected. You can also set VS Code to break on exceptions, allowing you to inspect variables at the moment where an exception occurs.

Testing multiplayer

You can test how your scripts behave with multiple players without needing more than one PC or multiple copies of the game! We recommend that you set the game to windowed mode in the settings for multiplayer tests.

When you are ready to test your scripts, start a regular multiplayer game with your package and make sure to set the connection mode to direct connect. Then go to the game folder (right click the game in Steam, select Properties->Local Files->Browse Local Files…). Start TabletopPlayground.exe for each additional player you want to test with. In the new game windows, go to “Join Game”, and then click on “Direct IP Connect” at the top. Enter 127.0.0.1 as the IP and click on Join. You will appear as a new player in your own game!

Handling exceptions

If an exception occurs in your code, you will see the error message and the location in the code printed in the in-game console. If you want to handle uncaught exceptions globally, for example to send them to a server that aggregates the errors that your players see, you can use $uncaughtException. Set it to a function that accepts a string, which will contain the error type, message, and location:

$uncaughtException = (error) => {
    console.log(error);
}