Scripting

You can click the scripting button in the toolbar to open the integrated scripting environment. The integrated scripting environment supports code auto-completion and IntelliSense. Type and syntax errors are automatically detected. All built-in types, functions, and properties are fully documented. Using the Scripting API you can create your own scripted components.

Scripted components can be used to add custom behaviours to entities in the virtual world. Some common examples include:

  • Control logic for robots and machines.
  • Data collection and visualization.
  • Modifying graphics properties at runtime.

You can create, delete, edit or rename your scripts. To create a new file, click the add button and enter a name for the script. You can left click on a file in the explorer to open it and right click on a file to rename or delete it. Newly created scripts will automatically include the minimum boilerplate code that is required for creating a scripted component:

import { Component, type Entity, type Handle } from "prototwin";

export class MyScriptedComponent extends Component {
    constructor(entity: Entity) {
        super(entity);
    }
}

The Component class is the base type for all components. All scripted components must extend this class, which includes some methods:

  • initialize - Function called immediately before simulating (at t=0) when the model is initialized.
  • update - Function called every timestep when simulating.
  • added - Function called immediately after the component is attached to an entity.
  • removed - Function called immediately after the component is detached from an entity.
  • subscribe - Subscribes to a subscribable object.
  • unsubscribe - Unsubscribes from a subscribable object.
  • handle - Creates a handle (weak reference) to a trackable object, such as a component or entity.

It is common practice to override the initialize and update functions:

import { Component, type Entity, Vec3 } from "prototwin";

export class MyScriptedComponent extends Component {
    public constructor(entity: Entity) {
        super(entity);
    }

    public override initialize(): void {
        // Called on model initialization.
        this.entity.name = "Oscillator"; // Rename the entity at t=0
    }

    public override update(dt: number): void {
        // Called every timestep.
        this.entity.position = new Vec3(0, Math.sin(this.world.time), 0);
    }
}

Referencing Entities / Components

It is possible to find entities by name:

export class MyScriptedComponent extends Component {
    public constructor(entity: Entity) {
        super(entity);
    }

    public override initialize(): void {
        const gantry = this.world.find("Gantry");
        if (gantry !== null) {
            console.log("Found the gantry!");
        }
    }
}

Note that this will search through the full list of entities in the virtual world, which may be computationally inefficient.

It is also possible to find a component on any given entity:

import { Component, type Entity, GraphicsComponent, PhysicsComponent, Vec3, Collider } from "prototwin";

export class MyScriptedComponent extends Component {
    public constructor(entity: Entity) {
        super(entity);
    }

    public override initialize(): void {
        const graphics = this.entity.findComponent(GraphicsComponent); // Find the graphics component, if it exists.
        if (graphics !== null) {
            graphics.scale = new Vec3(1, 2, 3); // Change the scale of the graphical mesh.
        }

        const physics = this.entity.component(PhysicsComponent); // Find or create the physics component.
        physics.collider = Collider.Convex; // Change the collider type to convex.
    }
}

The findComponent() function will attempt to find a component of the specified type on the entity, returning null if the component doesn’t exist. The component() function will additionally create the component if it doesn’t exist. Finding components using these functions will perform a linear search through the list of components attached to the entity, which may be computationally inefficient.

Handles

The generally preferred method of referencing other entities and components is by using handles. Handles automatically monitor the lifetime of the objects that they reference, so the value of the handle is automatically set to null if the object they reference is deleted. The value of a handle can be set through the inspector or programmatically.

import { Component, Entity, Handle, SensorComponent } from "prototwin";

export class MyScriptedComponent extends Component {
    // Declare the handles as public properties.
    public gantry: Handle<Entity>;
    public sensor: Handle<SensorComponent>;

    constructor(entity: Entity) {
        super(entity);

        // Initialize the handles.
        this.gantry = this.handle(Entity);
        this.sensor = this.handle(SensorComponent);
    }

    public override initialize(): void {
        // Access the values referenced by the handles.
        if (this.gantry.value !== null) {
            this.gantry.value.name = "Gantry";
        }

        if (this.sensor.value !== null) {
            this.subscribe(this.sensor.value.io.state, function(state: boolean) {
                console.log(`Sensor state changed: ${state}`)
            });
        }
    }
}

Decorators

The public properties of components are displayed in the inspector, provided that the types of those properties are supported. The following property types are visible/editable through the inspector:

It is possible to configure how the property is displayed/edited through the inspector using various decorators. For example, you can assign units to a numeric property using the @Units decorator. Similarly, you can display an enum property as a dropdown using the @Dropdown decorator. The @Flags decorator allows for multiple enum options to be selected.

An example is provided below that shows how to apply decorators to your public properties:

import { Component, type Entity, Slider, Name, Icon, Units, UnitType,
         Dropdown, Flags, Vec3, LocalFeature, LocalFeatureType,
         Category, Visible, Readonly, Label } from "prototwin";

export enum Options {
    Slow,
    Normal,
    Fast
}

export enum Outputs {
    None = 0,
    Distance = 1 << 0,
    Color = 1 << 1,
    Brightness = 1 << 2
}

@Icon("mdi:smiley") // Provide an icon for the component from https://icon-sets.iconify.design
@Name("Custom Name") // Provide a name for the component instead of using the class name
export class MyComponent extends Component {
    @Dropdown(Options) // Display the enumeration options as a dropdown
    public options: Options = Options.Normal;

    @Flags(Outputs) // Allow multiple enumeration options to be selected
    public outputs: Outputs = Outputs.Distance | Outputs.Color;

    @Slider(0, 10, 10) // Slider from 0 to 10 with 10 steps
    public range: number = 0;

    // Only display property if distance output is enabled
    @Visible((component: MyComponent) => (component.outputs & Outputs.Distance) === Outputs.Distance)
    @Units(UnitType.LinearDistance) // Display units of linear distance
    public distance: number = 10;

    @Readonly(true) // Display the property as readonly (required to display if there is no setter)
    @Label("Multiplied") // Provide a custom label for the property
    public get multipliedRange(): number {
        return this.range * 2;
    }

    @Category("My Category") // Group the property into a custom category
    @LocalFeature(LocalFeatureType.Position) // Allow point to be configured using feature selection
    public point: Vec3 = Vec3.zero;

    @Category("My Category")
    @LocalFeature(LocalFeatureType.Axis) // Allow direction to be configured using feature selection
    public direction: Vec3 = Vec3.zero;
    
    constructor(entity: Entity) {
        super(entity);
    }
}

The decorators are used by the inspector to control how the properties should be displayed/edited:

TypeScript Decorators