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 script selection dialog.
  4. Toggle on the Node Script you want to attach.
  5. Close the dialog.

The script takes effect immediately. You can attach multiple scripts to a single Node; they run in the order they were added.


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, the code is re-evaluated and a new instance is created: the constructor runs again. If you don't modify the script, the current instance stays active. 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);
}
}
}

Parameterizing a script via userData

node.userData also lets you use the same script class with different configuration on different Nodes. Set the parameters directly on the Node's userData in the Property Editor, and read them in the script's constructor:

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

update(delta) {
this.elapsed += delta;

// Read parameters from userData, with sensible defaults.
const amplitude = this.node.userData.amplitude ?? 2; // metres
const period = this.node.userData.period ?? 500; // milliseconds

this.node.x = this.originX + Math.sin((this.elapsed / period) * Math.PI * 2) * amplitude;
}
}

Attach this script to two different Nodes and give each a different userData.amplitude or userData.period value in the Property Editor. Both Nodes share the same script asset but behave independently.


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.

Beware of the ephemeral nature. Every time you save a script edit, the 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.