Node Scripts
A Node Script is a script (defined as a JavaScript class) that can be attached to a Node to provide it with extra functionality and/or behavior.
A few examples of such custom behaviors that can be provided via Node Scripts include the following:
- Generator: A script that generates copies (clones) of the Node it is attached to on regular intervals.
- Bumper: A script which converts the attached Node into a pinball-like bumper.
- Destroyer: A script which destroys anything that gets in touch with the attached Node.
- And many more ...
You can find more examples at Physion's assets repository
Some key points on Node Scripts:
- A Node Script is a JavaScript class.
- A Node Script is a Text Asset of the Scene.
- Node scripts are used to provide custom behaviors to the nodes they are attached to.
- A single Node can have zero or multiple Node Scripts attached to it.
Creating a Node Script
The following steps describe the process of adding a Node Script in your Scene.
Before adding a Node Script we first need to make sure that we have a Scene to work with.
If no Scene is currently open you'll first have to create one using: File Menu
⇒ New File
We first need to open the Assets Library of the Scene.
To do that click Edit Menu
⇒ Assets Library
.
Now we can create a new Text/JavaScript
asset which will be used to store our script.
To do that click +
⇒ Text
⇒ JavaScript
A new Text/JavaScript
asset has now been added to our Assets Library:
As we can see in the screenshot above, the new text asset is currently empty.
We now need to define our actual script. For this we have two options:
- Select a Node Script from the predefined set of scripts provided by Physion
- This is done by clicking the
Select...
button.
- This is done by clicking the
- Or write our own script.
In this case, we'll write our own, very simple script. To do that, type the following in the
integrated code editor and then click the Apply
button.
class HelloWorld {
constructor(node) {
node.text = "Hello World!";
}
}
You'll notice that as you type your class, the Name
field gets automatically updated. Although
you can manually change the Name
of your script, it is recommended to leave the suggested name.
Now click the Apply
button in order to save your script.
At this point, your NodeScript is saved in your Scene's Assets Library but it's not yet attached to any of your Scene's nodes. Let's see how we can put this new script in use.
Attaching a Node Script
Attaching a Node Script to a Node in your Scene allows you to enhance its functionality with custom behaviors. Here’s how you can do this step-by-step.
- Select the Node
- Use the Select Tool to choose the Node that you want to attach a Script to.
- Edit the 'Scripts' property of the selected Node
- Use the Property Editor, which displays the properties of the selected Node, and locate the
Scripts
property. - Click the
Edit...
button next to theScripts
property to open the script selection dialog. - Find the Node Script you want to attach, and toggle it on.
- Use the Property Editor, which displays the properties of the selected Node, and locate the
- Close the script selection dialog
Important Node Script methods
Below are the key methods that can be defined in a Node Script. Each method allows you to customize behavior at various stages of the Node's lifecycle.
constructor
When a NodeScript is attached to a Node, Physion calls its constructor, passing the associated Node as an argument. It is the responsibility of the script to store a reference to this Node. For example:
class MyScript {
constructor(node) {
this.node = node; // Save the reference to the passed Node for later use.
// Optionally initialize other variables that you might need in your script.
this.myVar = "example"; // A string variable.
this.myCounter = 0; // A numeric variable.
}
}
Node Scripts are designed to be ephemeral. This means that every time you update your script, its code is re-evaluated and a new instance of your class is created. As a result, the constructor runs each time you save changes. However, if you don’t modify the script, the current instance remains active. Keep this behavior in mind to avoid unintended side effects like duplicate event listeners or multiple initializations.
Often, a Node script is designed with a specific type of Node in mind. For instance, you might create a script that works exclusively with a JointNode because it adjusts joint specific properties. To ensure that the correct Node type is supplied, you can include a type-check in the constructor like this:
class BreakableJoint {
constructor(node) {
// Assign the node only if it is an instance of JointNode.
// Otherwise, set it to undefined.
this.node = node instanceof physion.JointNode ? node : undefined;
if (!this.node) {
console.warn("BreakableJoint can only be attached to a JointNode");
}
}
// ...
}
update
The update
method is one of the most frequently used in a Node Script.
If defined, it is called on every simulation step with the delta (in milliseconds) since the last update.
Here’s an example that assigns a random fillColor to the associated Node on every update:
class ColorChanger {
constructor(node) {
this.node = node;
}
update(delta) {
this.node.fillColor = physion.utils.randomColor();
}
}
physion.utils is a global object that exposes a wide range of utility functions designed to simplify common scripting tasks. For example, you can use:
- randomColor: Generates a random color.
- calculateDistance: Computes the distance between two points.
- clamp: Restricts a value within a specific range.
- many more..
destroy
To be documented...
onSceneStarted / onSceneStopped
The onSceneStarted
method is called when the Scene’s simulation begins, and the onSceneStopped
method is called when it stops.
Both methods accept a single argument: the Scene that has just started or stopped.
You can use these methods to perform initialization or cleanup tasks that will happen every time the Scene starts or stops.
class MyScript {
constructor(node) {
this.node = node;
}
onSceneStarted(scene) {
this.node.text = "Scene started!";
console.log("Scene started:", scene);
}
onSceneStopped(scene) {
this.node.text = "Scene stopped!";
console.log("Scene stopped:", scene);
}
}
onBeginContact / onEndContact
These methods only apply when the associated node is of type BodyNode (i.e. a physics body). They do not apply to nodes that are not subclasses of BodyNode.
The onBeginContact
method, if defined, is called when the associated BodyNode starts to make
contact with another object in the simulation. This method is particularly useful for handling
collision detection and response. For example, you might want to change the Node’s color when it
begins to touch another object. Similarly, the onEndContact
method is called when the collision ends.
Both methods accept two parameters:
- other: The other BodyNode involved in the collision.
- contact: A Box2D.b2Contact object that contains detailed information about the collision.
class ContactResponder {
constructor(node) {
this.node = node;
}
onBeginContact(other, contact) {
this.node.fillColor = "red";
}
onEndContact(other, contact) {
this.node.fillColor = "green";
}
}
In most cases, you might not need to use the contact parameter. However, if you want to perform more advanced collision handling, you can utilize the contact parameter to extract detailed information such as collision normals, impulse values, and other relevant metrics provided by the Box2D.b2Contact object.
It is important to not modify the state of the physics world (for example, by removing bodies) during these callbacks. The Box2D engine is still processing the contact, and altering the physics world at this stage can lead to unstable behavior or unexpected results.
If you need to remove a body as a result of a collision, a common workaround is to queue the removal and then process it in the update method. This way, you avoid modifying the physics world directly during the contact callbacks. Consider the following example:
class Destroyer {
constructor(node) {
this.node = node;
this.destroyQueue = new Set();
}
update(delta) {
// Process the destruction queue safely in the update loop
for (let bodyNode of this.destroyQueue) {
if (bodyNode.parent) {
bodyNode.parent.removeChild(bodyNode);
this.destroyQueue.delete(bodyNode);
}
}
}
onBeginContact(bodyNode, contact) {
if (contact.IsTouching()) {
// Instead of immediately removing the body, add it to a queue to be processed in the update method.
this.destroyQueue.add(bodyNode);
}
}
}