Needle Engine Documentation
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help

Scripting Examples

Code snippets and examples for common tasks in Needle Engine.

New to Scripting?

Start with these guides first:

  • TypeScript Essentials - Language basics
  • For Unity Developers - Unity → Web guide
  • Create Custom Components - Complete component guide
  • Video: How to write custom components

More Examples:

  • 100+ Sample Scenes - Live interactive examples
  • Stackblitz Collection - Edit in browser
  • API Documentation - Complete reference

🎯 Getting Started

Basic Component

Every component extends Behaviour and uses lifecycle methods like start() and update().

import { Behaviour, serializable } from "@needle-tools/engine"
import { Object3D } from "three"

export class MyComponent extends Behaviour {

    @serializable(Object3D)
    myObjectReference?: Object3D;

    start() {
        console.log("Hello world", this);
    }

    update() {
        this.gameObject.rotateY(this.context.time.deltaTime);
    }
}

See all lifecycle methods • Component Architecture


📦 References & Assets

Reference an Object from Unity

Reference scene objects and access them at runtime.

import { Behaviour, serializable, Camera } from "@needle-tools/engine";
import { Object3D } from "three"

export class MyClass extends Behaviour {
    // this will be a "Transform" field in Unity
    @serializable(Object3D) 
    myObjectReference: Object3D | null = null;
    
    // this will be a "Transform" array field in Unity
    // Note that the @serializable decorator contains the array content type! (Object3D and not Object3D[])
    @serializable(Object3D) 
    myObjectReferenceList: Object3D[] | null = null;

    // for component or other objects use the object's type
    @serializable(Camera)
    myCameraComponent: Camera | null = null;
}

How it works:

  1. In Unity/Blender, you assign an object to the field in the Inspector
  2. During export, the reference is saved in the glTF file with a unique identifier
  3. At runtime, Needle Engine resolves the reference and assigns the actual three.js Object3D to your field
  4. The reference is available in awake() or start() lifecycle methods

Type Safety

Use @serializable(Object3D) so TypeScript knows the type. Without it, you still get the reference, but as a plain object.

Understanding references • Serialization guide

Reference and Load a Prefab

Load prefabs or scene assets dynamically at runtime.

import { Behaviour, serializable, AssetReference } from "@needle-tools/engine";

export class MyClass extends Behaviour {

    // if you export a prefab or scene as a reference from Unity you'll get a path to that asset
    // which you can de-serialize to AssetReference for convenient loading
    @serializable(AssetReference)
    myPrefab?: AssetReference;
    
    async start() {
      // directly instantiate
      const myInstance = await this.myPrefab?.instantiate();

      // you can also just load and instantiate later
      // const myInstance = await this.myPrefab.loadAssetAsync();
      // this.gameObject.add(myInstance)
      // this is useful if you know that you want to load this asset only once because it will not create a copy
      // since ``instantiate()`` does create a copy of the asset after loading it
    }  
}

How it works:

  1. Assign a prefab or scene asset in Unity/Blender Inspector
  2. The asset path is exported to the glTF
  3. Use AssetReference to load the asset on demand
  4. Call instantiate() to create instances in your scene
  5. Assets are cached after first load for better performance

Lazy Loading

Prefabs are only loaded when you call instantiate(), not on scene load. This keeps initial load times fast!

AssetReference API • GameObject.instantiate

Reference and Load Scenes

Load and unload scenes dynamically for multi-scene projects.

import { Behaviour, serializable, AssetReference } from "@needle-tools/engine";

export class LoadingScenes extends Behaviour {
    // tell the component compiler that we want to reference an array of SceneAssets
    // @type UnityEditor.SceneAsset[]
    @serializable(AssetReference)
    myScenes?: AssetReference[];

    async awake() {
        if (!this.myScenes) {
            return;
        }
        for (const scene of this.myScenes) {
            // check if it is assigned in unity
            if(!scene) continue;
            // load the scene once
            const myScene = await scene.loadAssetAsync();
            // add it to the threejs scene
            this.gameObject.add(myScene);
            
            // of course you can always just load one at a time
            // and remove it from the scene when you want
            // myScene.removeFromParent();
            // this is the same as scene.asset.removeFromParent()
        }
    }

    onDestroy(): void {
        if (!this.myScenes) return;
        for (const scene of this.myScenes) {
            scene?.unload();
        }
    }
}

How it works:

  1. Reference Unity Scene assets or glTF files
  2. Scenes are exported as separate glTF files
  3. Use SceneSwitcher or load manually with AssetReference
  4. Previous scene is unloaded automatically (or kept loaded if desired)
  5. Perfect for level-based games or multi-room experiences

Live Example

Try the multi-scene loading sample

SceneSwitcher component • Scene management guide


🖱️ Input & Interaction

Receive Clicks on Objects

Make objects clickable by adding this script. Requires ObjectRaycaster component in parent hierarchy.

import { Behaviour, PointerEventData, showBalloonMessage } from "@needle-tools/engine";

export class ClickExample extends Behaviour {

    // Make sure to have an ObjectRaycaster component in the parent hierarchy
    onPointerClick(_args: PointerEventData) {
        showBalloonMessage("Clicked " + this.name);
    }
}

Input events reference • Interaction components

Networking Clicks (Multiplayer)

Sync clicks across all connected clients. The click event is sent to all users and can trigger animations, hide objects, etc.

import { Behaviour, EventList, PointerEventData, serializable } from "@needle-tools/engine";

export class SyncedClick extends Behaviour {

    @serializable(EventList)
    onClick!: EventList;

    onPointerClick(_args: PointerEventData) {
        console.log("SEND CLICK");
        this.context.connection.send("clicked/" + this.guid);
        this.onClick?.invoke();
    }

    onEnable(): void {
        this.context.connection.beginListen("clicked/" + this.guid, this.onRemoteClick);
    }
    onDisable(): void {
        this.context.connection.stopListen("clicked/" + this.guid, this.onRemoteClick);
    }


    onRemoteClick = () => {
        console.log("RECEIVED CLICK");
        this.onClick?.invoke();
    }
    
}

Unity/Blender Integration

Assign functions to the onClick event in Unity/Blender to trigger animations or other actions!

Networking guide • SyncedRoom component


🎬 Animation

Play Animation on Click

Trigger animations when objects are clicked.

import { Behaviour, serializable, Animation, PointerEventData } from "@needle-tools/engine";

export class PlayAnimationOnClick extends Behaviour {

    @serializable(Animation)
    animation?: Animation;

    awake() {
        if (this.animation) {
            this.animation.playAutomatically = false;
            this.animation.loop = false;
        }
    }

    onPointerClick(_args: PointerEventData) {
        if (this.animation) {
            this.animation.play();
        }
    }
}

Reference Animation Clips

Access animation clips for custom animation logic. You can also export arrays of clips.

import { Behaviour, serializable } from "@needle-tools/engine";
import { AnimationClip } from "three"

export class ExportAnimationClip extends Behaviour {

    @serializable(AnimationClip)
    animation?: AnimationClip;

    awake() {
        console.log("My referenced animation clip", this.animation);
    }
}

Animation components • Animator API


📢 Events & Serialization

Create and Invoke Unity Events

Use EventList to create events that can be configured in Unity/Blender Editor.

import { Behaviour, serializable, EventList } from "@needle-tools/engine"

export class MyComponent extends Behaviour {

    @serializable(EventList)
    myEvent? : EventList;

    start() {
        this.myEvent?.invoke();
    }
}

Component-Level Events

EventList events can also be subscribed to at the component level:

myComponent.addEventListener("my-event", evt => {...});

This is an experimental feature. Share feedback on our forum

Custom Event Types

Expose custom events with specific arguments (e.g., strings, numbers) to Unity/Blender.

import { Behaviour, serializable, EventList } from "@needle-tools/engine";
import { Object3D } from "three";

/*
Make sure to have a c# file in your project with the following content:

using UnityEngine;
using UnityEngine.Events;

[System.Serializable]
public class MyCustomUnityEvent : UnityEvent<string>
{
}

Unity documentation about custom events: 
https://docs.unity3d.com/ScriptReference/Events.UnityEvent_2.html

*/

// Documentation → https://docs.needle.tools/scripting

export class CustomEventCaller extends Behaviour {

    // The next line is not just a comment, it defines 
    // a specific type for the component generator to use.

    //@type MyCustomUnityEvent
    @serializable(EventList)
    myEvent!: EventList;

    // just for testing - could be when a button is clicked, etc.
    start() {
        this.myEvent.invoke("Hello");
    }
}

export class CustomEventReceiver extends Behaviour {

    logStringAndObject(str: string) {
        console.log("From Event: ", str);
    }
}

Unity Editor integration:

Custom event in Unity

Nested Objects & Serialization

Nest custom objects with automatic serialization using @serializable decorators.

TypeScript:

import { Behaviour, serializable } from "@needle-tools/engine";

// Documentation → https://docs.needle.tools/scripting

class CustomSubData {
    @serializable()
    subString: string = "";
    
    @serializable()
    subNumber: number = 0;
}

class CustomData {
    @serializable()
    myStringField: string = "";
    
    @serializable()
    myNumberField: number = 0;
    
    @serializable()
    myBooleanField: boolean = false;
    
    @serializable(CustomSubData)
    subData: CustomSubData | undefined = undefined;

    someMethod() {
        console.log("My string is " + this.myStringField, "my sub data", this.subData)
    }
}

export class SerializedDataSample extends Behaviour {

    @serializable(CustomData)  
    myData: CustomData | undefined;
    
    onEnable() {
        console.log(this.myData);
        this.myData?.someMethod();
    }
}

C# (Unity/Blender):

using System;

[Serializable]
public class CustomSubData
{
    public string subString;
    public float subNumber;
}
	
[Serializable]
public class CustomData
{
    public string myStringField;
    public float myNumberField;
    public bool myBooleanField;
    public CustomSubData subData;
}

Automatic Deserialization

Data without type decorators still works - you'll get plain objects. Add types as needed for strongly-typed access.

Type system guide • Serialization reference


🌐 Web APIs & Browser Features

Full Web Platform Access

Needle Engine gives you access to all web APIs and any npm package. Use geolocation, notifications, media devices, and more!

Browse npm packages • Web APIs reference

Geolocation - Display Current Location

Access the browser's Geolocation API to get the user's position.

import { Behaviour, showBalloonMessage } from "@needle-tools/engine";

export class WhereAmI extends Behaviour {
    start() {
        navigator.geolocation.getCurrentPosition((position) => {
            console.log("Navigator response:", position);
            const latlong = position.coords.latitude + ", " + position.coords.longitude;
            showBalloonMessage("You are at\nLatLong " + latlong);
        });
    }
}

Geolocation API docs

Display Current Time (with Coroutine)

Use coroutines for time-based updates without blocking the main thread.

import { Behaviour, Text, serializable, WaitForSeconds } from "@needle-tools/engine";

export class DisplayTime extends Behaviour {

    @serializable(Text)
    text?: Text;

    onEnable(): void {
        this.startCoroutine(this.updateTime())
    }

    private *updateTime() {
        while (true) {
            if (this.text) {
                this.text.text = new Date().toLocaleTimeString();
                console.log(this.text.text)
            }
            yield WaitForSeconds(1)
        }
    };
}

Coroutines guide


🎨 Rendering & Visual Effects

Change Custom Shader Properties

Modify shader properties at runtime (e.g., _Speed float value).

import { Behaviour, serializable } from "@needle-tools/engine";
import { Material } from "three";

declare type MyCustomShaderMaterial = {
   _Speed: number;
};

export class IncreaseShaderSpeedOverTime extends Behaviour {

   @serializable(Material)
   myMaterial?: Material & MyCustomShaderMaterial;

   update() {
       if (this.myMaterial) {
           this.myMaterial._Speed *= 1 + this.context.time.deltaTime;
           if(this.myMaterial._Speed > 1) this.myMaterial._Speed = .0005;
           if(this.context.time.frame % 30 === 0) console.log(this.myMaterial._Speed)
       }
   }
}
View on GitHub

Shader samples • Materials guide

Switching src Attribute

Dynamically change the scene being loaded.

Live example on StackBlitz

Adding Custom Post-Processing Effects

Create custom post-processing effects using the pmndrs/postprocessing library.

Installation:

npm i postprocessing

Add the effect to the same object as your Volume component. Example wrapping the Outline effect:

import { EffectProviderResult, PostProcessingEffect, registerCustomEffectType, serializable } from "@needle-tools/engine";
import { OutlineEffect } from "postprocessing";
import { Object3D } from "three";

export class OutlinePostEffect extends PostProcessingEffect {

    // the outline effect takes a list of objects to outline
    @serializable(Object3D)
    selection!: Object3D[];

    // this is just an example method that you could call to update the outline effect selection
    updateSelection() {
        if (this._outlineEffect) {
            this._outlineEffect.selection.clear();
            for (const obj of this.selection) {
                this._outlineEffect.selection.add(obj);
            }
        }
    }


    // a unique name is required for custom effects
    get typeName(): string {
        return "Outline";
    }

    private _outlineEffect: void | undefined | OutlineEffect;

    // method that creates the effect once
    onCreateEffect(): EffectProviderResult | undefined {

        const outlineEffect = new OutlineEffect(this.context.scene, this.context.mainCamera!);
        this._outlineEffect = outlineEffect;
        outlineEffect.edgeStrength = 10;
        outlineEffect.visibleEdgeColor.set(0xff0000);
        for (const obj of this.selection) {
            outlineEffect.selection.add(obj);
        }

        return outlineEffect;
    }
}
// You need to register your effect type with the engine
registerCustomEffectType("Outline", OutlinePostEffect);

Post-processing components • Volume API

Custom ParticleSystem Behaviour

Extend the ParticleSystem with custom behavior.

import { Behaviour, ParticleSystem } from "@needle-tools/engine";
import { ParticleSystemBaseBehaviour, QParticle } from "@needle-tools/engine";

// Derive your custom behaviour from the ParticleSystemBaseBehaviour class (or use QParticleBehaviour)
class MyParticlesBehaviour extends ParticleSystemBaseBehaviour {

    // callback invoked per particle
    update(particle: QParticle): void {
        particle.position.y += 5 * this.context.time.deltaTime;
    }
}
export class TestCustomParticleSystemBehaviour extends Behaviour {
    start() {
        // add your custom behaviour to the particle system
        this.gameObject.getComponent(ParticleSystem)!.addBehaviour(new MyParticlesBehaviour())
    }
}

ParticleSystem API • Particles sample


🔊 Audio & Media

Custom 2D Audio Component

Create a custom audio component (though AudioSource covers most use cases).

import { AudioSource, Behaviour, serializable } from "@needle-tools/engine";

// declaring AudioClip type is for codegen to produce the correct input field (for e.g. Unity or Blender)
declare type AudioClip = string;

export class My2DAudio extends Behaviour {

    // The clip contains a string pointing to the audio file - by default it's relative to the GLB that contains the component
    // by adding the URL decorator the clip string will be resolved relative to your project root and can be loaded
    @serializable(URL)
    clip?: AudioClip;

    awake() {
        // creating a new audio element and playing it
        const audioElement = new Audio(this.clip);
        audioElement.loop = true;
        // on the web we have to wait for the user to interact with the page before we can play audio
        AudioSource.registerWaitForAllowAudio(() => {
            audioElement.play();
        })
    }
}

AudioSource component • Audio guide


📁 File Handling

Load External Files

Use FileReference to load external files like JSON, images, or other assets.

import { Behaviour, FileReference, ImageReference, serializable } from "@needle-tools/engine";

export class FileReferenceExample extends Behaviour {

    // A FileReference can be used to load and assign arbitrary data in the editor. You can use it to load images, audio, text files... FileReference types will not be saved inside as part of the GLB (the GLB will only contain a relative URL to the file)
    @serializable(FileReference)
    myFile?: FileReference;
    // Tip: if you want to export and load an image (that is not part of your GLB) if you intent to add it to your HTML content for example you can use the ImageReference type instead of FileReference. It will be loaded as an image and you can use it as a source for an <img> tag.

    async start() {
        console.log("This is my file: ", this.myFile);
        // load the file
        const data = await this.myFile?.loadRaw();
        if (!data) {
            console.error("Failed loading my file...");
            return;
        }
        console.log("Loaded my file. These are the bytes:", await data.arrayBuffer());
    }
}

DropListener component • File API


HTMLHTML Integration

Receiving HTML Button Clicks in Components

Bridge HTML UI and 3D components by listening to DOM events.

How it works:

  1. Add a button or element in your HTML (index.html)
  2. Query the element with document.querySelector()
  3. Add event listener in your component's onEnable()
  4. Remove listener in onDisable() to prevent memory leaks

Example:

import { Behaviour } from "@needle-tools/engine";

export class HtmlButtonHandler extends Behaviour {
    private button?: HTMLButtonElement;

    onEnable() {
        this.button = document.querySelector("#my-button") as HTMLButtonElement;
        this.button?.addEventListener("click", this.onButtonClick);
    }

    onDisable() {
        this.button?.removeEventListener("click", this.onButtonClick);
    }

    private onButtonClick = () => {
        console.log("HTML button clicked!");
        // Trigger 3D scene changes here
    };
}

HTML DOM access • DOM Events


🔆 Environment & Lighting

Disable Environment Light

Control scene lighting by disabling environment maps at runtime.

import { Behaviour } from "@needle-tools/engine";

export class DisableEnvironment extends Behaviour {
    start() {
        // Disable environment map
        this.context.scene.environment = null;

        // Or reduce intensity
        if (this.context.scene.environment) {
            this.context.scene.environmentIntensity = 0.5;
        }
    }
}

three.js Scene.environment • Lighting guide


🤖 Advanced Integrations

MediaPipe Hand Tracking

Control your 3D scene with hand gestures using Google's MediaPipe library.

Installation:

npm i @mediapipe/hands @mediapipe/camera_utils

Try it live: MediaPipe Hands Sample (requires webcam)

How it works:

  1. MediaPipe tracks hand landmarks from webcam feed
  2. Hand positions are mapped to 3D world coordinates
  3. Use landmarks to control objects, UI, or interactions
  4. Runs in real-time with excellent performance

MediaPipe Hands docs • GitHub sample


⚛️ Physics Examples

Change Color on Collision

Detect physics collisions and change material colors.

import { Behaviour, Collision } from "@needle-tools/engine";

export class ColorOnCollision extends Behaviour {
    onCollisionEnter(col: Collision) {
        const renderer = this.gameObject.getComponent(Renderer);
        if (renderer?.sharedMaterial) {
            renderer.sharedMaterial.color.setHex(0xff0000); // Red
        }
    }
}

Physics events • Collision API

Physics Trigger Relay

Invoke events when objects enter/exit trigger zones.

import { Behaviour, Collision, EventList, serializable } from "@needle-tools/engine";

export class PhysicsTriggerRelay extends Behaviour {
    @serializable(EventList)
    onEnter?: EventList;

    @serializable(EventList)
    onExit?: EventList;

    onTriggerEnter(col: Collision) {
        this.onEnter?.invoke();
    }

    onTriggerExit(col: Collision) {
        this.onExit?.invoke();
    }
}

Trigger events • EventList guide

Auto Reset Position

Reset object position when it leaves a trigger zone (e.g., respawn objects).

import { Behaviour, Collision, serializable } from "@needle-tools/engine";
import { Vector3 } from "three";

export class AutoReset extends Behaviour {
    private startPosition?: Vector3;

    awake() {
        this.startPosition = this.gameObject.position.clone();
    }

    onTriggerExit(col: Collision) {
        if (this.startPosition) {
            this.gameObject.position.copy(this.startPosition);
        }
    }
}

Play Audio on Collision

Trigger sound effects on physics collisions.

import { Behaviour, Collision, AudioSource } from "@needle-tools/engine";

export class PlayAudioOnCollision extends Behaviour {
    private audio?: AudioSource;

    awake() {
        this.audio = this.gameObject.getComponent(AudioSource);
    }

    onCollisionEnter(col: Collision) {
        this.audio?.play();
    }
}

AudioSource component • Audio guide

Physics documentation • Rapier physics engine


🎲 Utility Scripts

Set Random Color

Randomize object colors on start (useful for spawned objects, testing).

import { Behaviour, Renderer } from "@needle-tools/engine";
import { Color } from "three";

export class SetRandomColor extends Behaviour {
    start() {
        const renderer = this.gameObject.getComponent(Renderer);
        if (renderer?.sharedMaterial) {
            // Clone material to avoid affecting other objects
            renderer.sharedMaterial = renderer.sharedMaterial.clone();
            renderer.sharedMaterial.color = new Color(
                Math.random(),
                Math.random(),
                Math.random()
            );
        }
    }
}

Material Cloning

Always clone materials when modifying them to avoid affecting other objects using the same material!

Spawn Objects Over Time

Instantiate objects at intervals (e.g., enemy spawning, particle effects).

import { Behaviour, AssetReference, serializable } from "@needle-tools/engine";

export class TimedSpawn extends Behaviour {
    @serializable(AssetReference)
    prefab?: AssetReference;

    @serializable()
    interval: number = 1; // seconds

    @serializable()
    maxCount: number = 10;

    private spawnCount = 0;

    async start() {
        while (this.spawnCount < this.maxCount) {
            await new Promise(resolve => setTimeout(resolve, this.interval * 1000));

            if (this.prefab) {
                const instance = await this.prefab.instantiate();
                if (instance) {
                    instance.position.copy(this.gameObject.position);
                    this.spawnCount++;
                }
            }
        }
    }
}

Prefab loading • Coroutines alternative

Suggest changes
Last Updated: 1/27/26, 8:30 PM

On this page

Extras

Copy for AI (LLMs)