Components
Components can be used to add custom behavior to entities in the virtual world. Some common examples include:
- Control logic for industrial machines and robots.
- Data collection and visualization.
- Creating or deleting entities in the digital world.
- Modifying component properties at runtime.
Creating a Component
Create a scripted component by right-clicking in the file explorer, then selecting New Item from the context menu. This will open a dialog menu, allowing you to select from one of the templates. Select Component from the dropdown and enter a name. ProtoTwin will automatically generate the minimum boilerplate code required to create the component:
import { type Entity, type Handle, Component } from "prototwin";
export class Script extends Component {
constructor(entity: Entity) {
super(entity);
}
public override initialize(): void {
// Called on model initialization.
}
public override update(dt: number): void {
// Called every timestep.
}
}Methods
The Component class is the base type for all components. All scripted components must extend this class, which includes some methods that may be useful:
- The
initializemethod is called immediately before simulating (at t=0) when the model is initialized. - The
updatemethod is called every timestep when simulating. - The
addedmethod called immediately after the component is attached to an entity. - The
removedmethod is called immediately after the component is detached from an entity. - The
subscribemethod subscribes to a subscribable object. - The
unsubscribemethod unsubscribes from a subscribable object. - The
handlemethod creates a handle (weak reference) to a trackable object, such as a component or entity.
Referencing Entities and Components
It is possible to find entities by name:
import { type Entity, Component } from "prototwin";
export class Script extends Component {
public constructor(entity: Entity) {
super(entity);
}
public override initialize(): void {
const main = this.world.find("Main");
if (main !== null) {
console.log("Found the main entity!");
}
}
}Note that this will search through the full list of entities in the virtual world, which may be computationally inefficient. It’s also possible to find a component on any given entity:
import { type Entity, Component, GraphicsComponent, PhysicsComponent, Vec3, Collider } from "prototwin";
export class Script 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 does not exist. Finding components with these functions performs a linear search over the list of components attached to the entity.
Handles
The preferred method of referencing entities and components is using handles. Handles automatically monitor the lifetime of the objects they reference, so the value of a handle is automatically set to null if the object they reference is deleted. The value of a handle can be set programmatically or through the inspector. The example below demonstrates how handles can be used to turn a conveyor motor off when the sensor is blocked.
import { Entity, Component, Handle, SensorComponent, MotorComponent } from "prototwin";
export class Controller extends Component {
// Declare and initialize the handles as public properties.
public target: Handle<Entity> = this.handle(Entity);
public sensor: Handle<SensorComponent> = this.handle(SensorComponent);
public conveyor: Handle<MotorComponent> = this.handle(MotorComponent);
constructor(entity: Entity) {
super(entity);
}
public override initialize(): void {
// Access the values referenced by the handles.
if (this.target.value !== null) {
this.target.value.name = "Target";
}
// Turn the conveyor motor off when the sensor is blocked, then turn it back on when the sensor is cleared.
if (this.sensor.value !== null) {
this.subscribe(this.sensor.value.io.state, (state: boolean) => {
this.conveyor.value!.io.state = !state;
});
}
}
}Custom IO
The properties of components are not accessible by external hardware (such as PLCs) or external software (such as Python clients). To create signals that can be accessed externally, you can create a custom IO class for your component. The example below demonstrates a simple component which generates a sinusoidal wave. It provides a writable (input) signal for the frequency of the wave and a readable (output) signal for the amplitude of the wave.
import { type Entity, Component, IO, DoubleSignal, Access, Units, UnitType } from "prototwin";
export class SineWaveGeneratorIO extends IO {
public frequency: DoubleSignal;
public amplitude: DoubleSignal;
public constructor() {
super();
this.frequency = new DoubleSignal(1, Access.Writable); // Input (writable)
this.amplitude = new DoubleSignal(0, Access.Readable); // Output (readable)
}
}
export class SineWaveGenerator extends Component {
#io: SineWaveGeneratorIO;
// IMPORTANT: Don't forget to add a public getter and setter for the IO
public override get io(): SineWaveGeneratorIO {
return this.#io;
}
public override set io(value: SineWaveGeneratorIO) {
this.#io = value;
}
@Units(UnitType.Frequency)
public get frequency(): number {
return this.#io.frequency.value;
}
public set frequency(value: number) {
this.#io.frequency.value = value;
}
constructor(entity: Entity) {
super(entity);
this.#io = new SineWaveGeneratorIO();
}
public override update(dt: number) {
this.#io.amplitude.value = Math.sin(2 * Math.PI * this.frequency * this.entity.world.time);
}
}