Skip to main content

Node Scripts

A Node Script is a JavaScript class attached to a Node to give it custom behavior or functionality.

A few examples of what you can build with Node Scripts:

  • Generator: Spawns clones of the attached Node at regular intervals.
  • Bumper: Turns the attached Node into a pinball-style bumper that repels objects on contact.
  • Destroyer: Removes any Node that touches the attached Node.
  • Laser Target: Counts how many times the attached Node has been hit by a laser.

You can find more examples at Physion's assets repository.

Key points:

  • A Node Script is a JavaScript class stored as a Text/JavaScript Asset of the Scene.
  • A single Node can have zero or more Node Scripts attached to it.
  • Scripts define optional lifecycle methods. Only the methods you declare will be called.
  • All scripts have access to the physion global object, which exposes every Node class and a utility library.

Creating a Node Script

info

Make sure you have a Scene open before adding a script. If no Scene is open, create one via File MenuNew File.

Open the Assets Library of the Scene via Edit MenuAssets Library.

Create a new Text/JavaScript asset by clicking +TextJavaScript.

Adding a text/javascript asset

A new empty Text/JavaScript asset appears in your Assets Library:

To define the script you have two options:

  • Click Select... to choose from Physion's predefined script library.
  • Write your own script directly in the integrated code editor.

Type the following minimal script and click Apply:

class HelloWorld {
constructor(node) {
node.text = "Hello World!";
}
}

As you type, the Name field updates automatically to match your class name. It's recommended to leave this as suggested.

Click Apply to save. The script is now stored in the Scene's Assets Library but not yet attached to any Node.


Attaching a Node Script

  1. Select the Node using the Select Tool.
  2. In the Property Editor, locate the Scripts property.
  3. Click Edit... next to Scripts to open the Scripts Editor popover.
  4. At the bottom of the popover, pick a Text/JavaScript asset from the dropdown and click Add.

The script takes effect immediately. The popover lists every script currently attached to the Node as a collapsible row showing its name and editable parameters. Each row also has:

  • A duplicate button, which attaches a second, independent instance of the same script (with the same parameter values, which you can then tweak separately).
  • A remove button, which detaches that instance.

You can attach multiple scripts to a single Node, including multiple instances of the same script asset each configured differently. They run in the order they appear in the list.


Script Parameters

A Node Script can expose some of its internal values as parameters that appear directly in the Scripts Editor row for that script, editable without touching code. This is the recommended way to make a script configurable.

Declare a parameter by adding a static PD_<name> field to the class. Its shape is the same property descriptor used throughout Physion's own Property Editor:

class Generator {
static PD_frequency = { path: "frequency", defaultValue: 180, min: 30, step: 10 };

constructor(node) {
this.node = node;
this.frequency = Generator.PD_frequency.defaultValue;
}
}
  • The field must be named PD_<name>, and path must match <name> exactly.
  • defaultValue is required; it seeds both the Scripts Editor's initial display and the value your constructor should read.
  • min, max, and step constrain numeric parameters.

Use editor: "Select" with selectOptions to turn a parameter into a dropdown:

class NotePlayer {
static PD_synthPreset = {
path: "synthPreset",
defaultValue: "Kalimba",
editor: "Select",
selectOptions: [
{ value: "Kalimba", label: "Kalimba" },
{ value: "Marimba", label: "Marimba" },
{ value: "Cello", label: "Cello" },
// ...
],
};

constructor(node) {
this.node = node;
this.synthPreset = NotePlayer.PD_synthPreset.defaultValue;
}
}
Parameters are merged onto the running instance, not passed to the constructor

When you edit a parameter's value in the Scripts Editor, Physion assigns the new value directly onto the already-running script instance (this.<name> = newValue); it does not re-run the constructor. For plain fields, this just works. If your script needs to react to a parameter change, e.g. restart an animation or recompute cached state, declare it as a getter/setter pair instead of a plain field and put the side effect in the setter:

class PropertyAnimator {
static PD_duration = { path: "duration", defaultValue: 3000, step: 1000 };

constructor(node) {
this.node = node;
this._duration = PropertyAnimator.PD_duration.defaultValue;
}

get duration() { return this._duration; }
set duration(v) {
if (this._duration !== v) {
this._duration = v;
this.updateAnimation(); // react to the change
}
}
}

Because each attached script is its own instance, every entry in the Scripts Editor, including duplicates of the same script asset, keeps its own independent parameter values.


Lifecycle Reference

All methods are optional. Physion calls only the methods that are defined in your class.

MethodCalled whenApplies to
constructor(node)Script is instantiatedAll nodes
update(delta)Every simulation stepAll nodes
destroy()Script is replaced or node is removedAll nodes
onSceneStarted(scene)Simulation startsAll nodes
onSceneStopped(scene)Simulation stopsAll nodes
onBeginContact(other, contact)Physics contact beginsBodyNode only
onEndContact(other, contact)Physics contact endsBodyNode only
onHitByLaser(laserNode, segment)This node is hit by a laserBodyNode, ParticleGroupNode
onLaserHit(node, segment)This laser hits another nodeLaserNode only

Method Details

constructor

When a Node Script is attached to a Node, Physion instantiates the class and passes the Node as the sole argument. Store a reference to it for use in other methods:

class MyScript {
constructor(node) {
this.node = node;

this.myVar = "example";
this.myCounter = 0;
}
}
Ephemeral Nature of Node Scripts

Every time you save changes to a script's code (or switch which asset a script entry points to), that entry's instance is destroyed and a new one is created: the constructor runs again. Other scripts attached to the same Node are unaffected. Editing a script's parameters in the Scripts Editor never recreates the instance. Keep this in mind to avoid duplicate event listeners or repeated side effects.

If your script is designed for a specific Node type, add a type-check in the constructor:

class BreakableJoint {
constructor(node) {
this.node = node instanceof physion.JointNode ? node : undefined;
if (!this.node) {
console.warn("BreakableJoint can only be attached to a JointNode");
}
}
}

update

Called on every simulation step. The delta argument is the time in milliseconds since the last step. Use this method for anything that needs to run continuously during the simulation.

class ColorChanger {
constructor(node) {
this.node = node;
}

update(delta) {
this.node.fillColor = physion.utils.randomColor();
}
}

A time-based movement example using delta to keep speed frame-rate independent:

class Oscillator {
constructor(node) {
this.node = node;
this.elapsed = 0;
this.originX = node.x;
}

update(delta) {
this.elapsed += delta;
this.node.x = this.originX + Math.sin(this.elapsed / 500) * 2;
}
}

destroy

Called automatically when the script is replaced (e.g. you save a new version) or the Node is removed from the Scene. Use it to clean up timers, event listeners, or any other resources your script created.

class PropertyChangeMonitor {
constructor(node) {
this.node = node;
this.node.on("propertyChanged", this.onPropertyChanged);
}

destroy() {
this.node.off("propertyChanged", this.onPropertyChanged);
}

onPropertyChanged = (name, value) => {
console.log(`Property '${name}' changed to ${value}`);
}
}

onSceneStarted / onSceneStopped

onSceneStarted is called when the simulation begins; onSceneStopped is called when it stops. Both receive the Scene as their argument. Use them for per-run initialization and cleanup.

class MyScript {
constructor(node) {
this.node = node;
}

onSceneStarted(scene) {
this.node.text = "Running…";
}

onSceneStopped(scene) {
this.node.text = "Stopped.";
}
}

onBeginContact / onEndContact

info

These methods only apply to BodyNode subclasses (circles, rectangles, polygons, text bodies, etc.). They are not called for non-physics nodes.

onBeginContact is called when the Node begins touching another object; onEndContact is called when the contact ends.

Both methods receive:

  • other: the other BodyNode involved in the collision.
  • contact: the raw Box2D.b2Contact object with low-level collision data.
class ContactResponder {
constructor(node) {
this.node = node;
}

onBeginContact(other, contact) {
this.node.fillColor = 0xff0000;
}

onEndContact(other, contact) {
this.node.fillColor = 0x00ff00;
}
}
Do not modify the physics world inside contact callbacks

The Box2D engine is still mid-step when these callbacks fire. Adding or removing bodies at this point can cause crashes or unstable behavior. Instead, queue the work and process it in update:

class Destroyer {
constructor(node) {
this.node = node;
this.destroyQueue = new Set();
}

update(delta) {
for (let bodyNode of this.destroyQueue) {
if (bodyNode.parent) {
bodyNode.parent.removeChild(bodyNode);
this.destroyQueue.delete(bodyNode);
}
}
}

onBeginContact(bodyNode, contact) {
if (contact.IsTouching()) {
this.destroyQueue.add(bodyNode);
}
}
}

onHitByLaser

info

This method applies to BodyNode and ParticleGroupNode (the node being struck by the laser).

Called each time a laser beam from a LaserNode strikes this Node. Useful for targets, sensors, and destructible objects.

Parameters:

  • laserNode: the LaserNode that fired the beam.
  • segment: details about the hit:
    • p1: start point of the laser segment
    • p2: end point (the hit point on this node)
    • length: length of this segment
    • bounceIndex: 0 for the initial cast, 1+ for each reflection
    • result: raycast result with hit point, normal, fixture, and more
class LaserTarget {
constructor(node) {
this.node = node;
this.hitCount = 0;
}

onHitByLaser(laserNode, segment) {
this.hitCount++;

if ("text" in this.node) {
this.node.text = this.hitCount.toString();
}
}
}

onLaserHit

info

This method applies to LaserNode only (the node that is emitting the laser).

The counterpart to onHitByLaser: called on the LaserNode whenever its beam strikes another object. Useful for laser behaviors that depend on what is being hit.

Parameters:

  • node: the BodyNode or ParticleGroupNode that was hit.
  • segment: same structure as in onHitByLaser.
class ColorChangingLaser {
constructor(node) {
this.node = node;
}

onLaserHit(other, segment) {
if ("fillColor" in other) {
other.fillColor = physion.utils.randomColor();
}
}
}

The physion Global

All scripts have access to the physion global object, which is the primary API for scripting in Physion.

Node classes

Every Node class is available as physion.<ClassName>, making it easy to perform instanceof checks or construct new nodes:

ClassDescription
physion.NodeBase class for all nodes
physion.SceneThe scene container
physion.BodyNodeBase class for physics bodies
physion.CircleNodeCircle physics body
physion.RectangleNodeRectangle physics body
physion.CapsuleNodeCapsule physics body
physion.PolygonNodeCustom polygon physics body
physion.RegularPolygonNodeRegular polygon physics body
physion.TextNodeText rendered as a physics body
physion.SpriteNodeImage sprite
physion.AnimatedSpriteNodeAnimated sprite
physion.LaserNodeLaser raycast emitter
physion.JointNodeBase class for joints
physion.RevoluteJointNodeRotational joint
physion.DistanceJointNodeFixed-distance joint
physion.WeldJointNodeRigid connection joint
physion.WheelJointNodeWheel/suspension joint
physion.SpringNodeSpring constraint
physion.ParticleGroupNodeLiquidFun particle group
physion.ParticleEmitterNodeParticle emitter
physion.TracerNodeTraces a body's movement path
physion.GraphNodeGraph/chart display

physion.utils

physion.utils is a library of utility functions for common scripting tasks. A selection of frequently used functions:

Math

FunctionDescription
randomNumber(min, max)Random number in the given range
clamp(value, min, max)Restrict a value to a range
lerp(a, b, t)Linear interpolation between two numbers
lerpPoint(a, b, t)Linear interpolation between two points
calculateDistance(p1, p2)Euclidean distance between two points
calculateAngle(p1, p2)Angle in radians from p1 to p2
radToDeg(rad)Radians → degrees
degToRad(deg)Degrees → radians
midPoint(p1, p2)Midpoint between two points

Color

FunctionDescription
randomColor()Random hex color number
darkenColor(color, amount)Darken a color by a percentage
lightenColor(color, amount)Lighten a color by a percentage
toHexString(color)Convert a color number to "#rrggbb"
fromHexString(hex)Convert "#rrggbb" to a color number

Nodes

FunctionDescription
cloneNode(node)Shallow-clone a node
cloneNodeDeep(node)Deep-clone a node and its children
filteredNodes(nodes, Class)Filter an array of nodes by class
getJointNodesOf(bodyNode)Get all joints connected to a body
createRope(p1, p2, segmentSize, ...)Build a rope of connected bodies

Geometry

FunctionDescription
rotatePoint(p, center, angle)Rotate a point around a center
createRegularPolygon(x, y, r, sides, rotation)Generate a regular polygon's points

physion.root

physion.root gives advanced scripts access to the application controller, including sceneController and commandManager. This is an escape hatch for power users; most scripts won't need it.


Node Types & Properties

The properties available to a script depend on the Node type. All values are readable and writable unless noted.

All nodes

PropertyTypeDescription
xnumberX position in scene units (meters)
ynumberY position in scene units (meters)
anglenumberRotation in degrees
alphanumberOpacity (01)
namestringDisplay name
idstringUnique identifier (read-only)
userDataobjectFree-form data storage
parentNodeParent node (read-only)
childrenNode[]Child nodes (read-only)

Key methods: addChild(node), removeChild(node), findChildByName(name), getDescendants().

GraphicsNode (shapes, sprites, text)

Adds visual properties:

PropertyTypeDescription
fillColornumberFill color as a hex number (e.g. 0xff0000)
fillAlphanumberFill opacity (01)
fillTexturestringImage asset ID for texture fill
lineColornumberStroke color
lineAlphanumberStroke opacity
lineWidthnumberStroke width
blendModenumberPIXI blend mode

BodyNode (physics bodies)

Adds physics properties:

PropertyTypeDescription
bodyTypestring"static", "kinematic", or "dynamic"
linearVelocityXnumberHorizontal velocity
linearVelocityYnumberVertical velocity
linearVelocity{x, y}Velocity vector (read-only)
angularVelocitynumberRotational velocity
linearDampingnumberVelocity damping
angularDampingnumberRotational damping
frictionnumberSurface friction (01)
restitutionnumberBounciness (01)
densitynumberMass per area
massnumberComputed mass (read-only)
gravityScalenumberScale factor on gravity
sensorbooleanDetect collisions without physical response
activebooleanEnable/disable the body
fixedRotationbooleanPrevent rotation
bulletbooleanEnable continuous collision (fast-moving objects)

LaserNode

PropertyTypeDescription
poweredbooleanTurn the laser on/off
colornumberLaser beam color
sizenumberBeam thickness
maxDistancenumberMaximum beam length
maxBouncesnumberNumber of reflections allowed
fadeWithDistancebooleanFade the beam over distance

JointNode

PropertyTypeDescription
bodyNodeAidstringID of the first connected body
bodyNodeBidstringID of the second connected body
anchorA{x, y}Anchor point on body A (local coords)
anchorB{x, y}Anchor point on body B (local coords)

Multiple Scripts on One Node

You can attach any number of scripts to a single Node. Physion calls each script's methods in the order they were attached.

Sharing state between scripts

Scripts on the same Node can share state through node.userData:

// Script A: writes to userData
class ScriptA {
constructor(node) {
this.node = node;
node.userData.score = 0;
}

onBeginContact(other, contact) {
this.node.userData.score += 1;
}
}

// Script B: reads from userData
class ScriptB {
constructor(node) {
this.node = node;
}

update(delta) {
if ("text" in this.node) {
this.node.text = String(this.node.userData.score ?? 0);
}
}
}

Configuring a script differently per attachment

To use the same script class with different configuration, whether on different Nodes or attached more than once to the same Node, don't reach for userData: declare the configurable values as Script Parameters instead. Each attached instance keeps its own parameter values, editable directly in the Scripts Editor, with no extra wiring required.


Best Practices

Always store the node reference in the constructor. Other lifecycle methods have no other way to access it.

Type-check when your script targets a specific node type. Using instanceof physion.JointNode (or whichever type) in the constructor prevents silent failures when a script is accidentally attached to the wrong node.

Keep update lean. It runs every simulation step. Avoid allocating objects or doing heavy computation inside it.

Never modify the physics world during contact callbacks. Queue removals or property changes in a Set and process them in update. See the Destroyer example above.

Clean up in destroy. If your script registers event listeners, clear them in destroy to prevent leaks and phantom behavior when the script is replaced.

Use node.userData for cross-script communication. It's the intended shared data store between multiple scripts on the same Node.

Use PD_ static properties for user-configurable values. They show up as editable fields in the Scripts Editor and keep independent values per attached instance. Reserve userData for cross-script communication or internal state that shouldn't be user-editable.

Beware of the ephemeral nature. Every time you save an edit to a script's code (or swap its asset), that entry's constructor runs again. Avoid side effects in the constructor that should happen only once (e.g. adding children), or guard them with a check on node.userData. Editing a script's parameters does not trigger this; use a getter/setter if you need to react to a parameter change instead.