Needle Engine 文档
Downloads
  • What is Needle Engine?
  • 用户评价
  • Get an overview

    • Samples and Showcase
    • 我们的愿景 🔮
    • 功能概览
    • 技术概述
  • Resources

    • Pricing and Plans
    • Changelog
    • API Documentation
    • Support & Community
  • Integrations

    • Unity 版 Needle Engine
    • Needle Engine for Blender
    • Needle Engine 作为 Web Component
    • 在您的网站上使用 Needle Engine
    • Needle Cloud
  • Topics

    • Web 项目结构
    • Everywhere Actions
    • Exporting Assets to glTF
    • 框架、打包器、HTML
    • Testing on local devices
    • 部署与优化
  • Advanced

    • 网络
    • VR & AR (WebXR)
    • 使用 Needle Engine 直接从 HTML
    • 编辑器同步
  • Troubleshooting

    • 如何调试
    • 常见问题 (FAQ) 💡
    • Get Help
  • Videos

    • Tutorials on Youtube
    • Interviews on Youtube
  • Scripting Overview

    • 在 Needle Engine 中编写脚本
    • Scripting Introduction for Unity Developers
    • Needle 核心组件
    • Everywhere Actions
  • Components and Lifecycle

    • Creating and using Components
    • @serializable and other decorators
    • 自动生成组件
    • Scripting Examples
    • Community Contributions
    • 附加模块
  • Settings and APIs

    • <needle-engine> 配置
    • needle.config.json
    • Needle Engine API
    • three.js API
Help
Samples
Pricing
  • Needle Website
  • Needle Cloud
  • Support Community
  • Discord Server
  • X/Twitter
  • YouTube
  • Newsletter
  • Email
  • Feedback
  • Github
  • English
  • 简体中文
  • Español
  • Português
  • Français
  • हिन्दी
  • 日本語
  • Deutsch
  • Tiếng Việt
Downloads
  • What is Needle Engine?
  • 用户评价
  • Get an overview

    • Samples and Showcase
    • 我们的愿景 🔮
    • 功能概览
    • 技术概述
  • Resources

    • Pricing and Plans
    • Changelog
    • API Documentation
    • Support & Community
  • Integrations

    • Unity 版 Needle Engine
    • Needle Engine for Blender
    • Needle Engine 作为 Web Component
    • 在您的网站上使用 Needle Engine
    • Needle Cloud
  • Topics

    • Web 项目结构
    • Everywhere Actions
    • Exporting Assets to glTF
    • 框架、打包器、HTML
    • Testing on local devices
    • 部署与优化
  • Advanced

    • 网络
    • VR & AR (WebXR)
    • 使用 Needle Engine 直接从 HTML
    • 编辑器同步
  • Troubleshooting

    • 如何调试
    • 常见问题 (FAQ) 💡
    • Get Help
  • Videos

    • Tutorials on Youtube
    • Interviews on Youtube
  • Scripting Overview

    • 在 Needle Engine 中编写脚本
    • Scripting Introduction for Unity Developers
    • Needle 核心组件
    • Everywhere Actions
  • Components and Lifecycle

    • Creating and using Components
    • @serializable and other decorators
    • 自动生成组件
    • Scripting Examples
    • Community Contributions
    • 附加模块
  • Settings and APIs

    • <needle-engine> 配置
    • needle.config.json
    • Needle Engine API
    • three.js API
Help
Samples
Pricing
  • Needle Website
  • Needle Cloud
  • Support Community
  • Discord Server
  • X/Twitter
  • YouTube
  • Newsletter
  • Email
  • Feedback
  • Github
  • English
  • 简体中文
  • Español
  • Português
  • Français
  • हिन्दी
  • 日本語
  • Deutsch
  • Tiếng Việt
  • Getting Started

    • Downloads
    • Needle Engine for Unity
    • Needle Engine for Blender
    • Needle Engine as Web Component
    • Needle Engine on your Website
    • Needle Cloud
    • Custom integrations
    • Support and Community
  • Core Concepts

    • Web 项目结构
    • Everywhere Actions
    • Exporting Assets to glTF
    • 框架、打包器、HTML
    • Testing on local devices
    • 部署与优化
    • 如何调试
    • 常见问题 (FAQ) 💡
  • Scripting

    • 在 Needle Engine 中编写脚本
    • Scripting Introduction for Unity Developers
    • Creating and using Components
    • 自动生成组件
    • Scripting Examples
    • Community Contributions
  • Advanced

    • VR & AR (WebXR)
    • 网络
    • 编辑器同步
  • Reference

    • 功能概览
    • 技术概述
    • Needle 核心组件
    • needle.config.json
    • <needle-engine> 配置
    • @serializable and other decorators

Scripting Examples

If you are new to scripting we highly recommend reading the following guides first:

  • Beginner Guide: Typescript Essentials
  • Beginner Guide: Needle Engine for Unity Developers
  • Video tutorial: How to write custom components

Below you will find a few basic scripts as a quick reference.

We also offer a lot of sample scenes and complete projects that you can download and use as a starting point:

  • Visit Samples Website
  • Download Samples Package
  • Needle Engine Stackblitz Collection
  • Needle Engine API

Basic component

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 scripting for all component events

Reference an Object from Unity

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;
}

Reference and load an asset from Unity (Prefab or SceneAsset)

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
    }  
}

Reference and load scenes from Unity

提示

Find a working example in our samples to download and try

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();
        }
    }
}

Receive Clicks on Objects

Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object.

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

export class ClickExample extends Behaviour implements IPointerClickHandler {

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

Networking Clicks on Objects

Add this script to any object in your scene that you want to be clickable. Make sure to also have an ObjectRaycaster component in the parent hierarchy of that object.
The component will send the received click to all connected clients and will raise an event that you can then react to in your app. If you are using Unity or Blender you can simply assign functions to call to the onClick event to e.g. play an animation or hide objects.

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

export class SyncedClick extends Behaviour implements IPointerClickHandler {

    @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();
    }
    
}

Play Animation on click

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

export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler {

    @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 an Animation Clip

This can be useful if you want to run your custom animation logic.
You can also export an array 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);
    }
}

Create and invoke a UnityEvent

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

export class MyComponent extends Behaviour {

    @serializable(EventList)
    myEvent? : EventList;

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

提示

EventList events are also invoked on the component level. This means you can also subscribe to the event declared above using myComponent.addEventListener("my-event", evt => {...}) as well.
This is an experimental feature. Please provide feedback in our forum

Declare a custom event type

This is useful for when you want to expose an event to Unity or Blender with some custom arguments (like a string)

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);
    }
}

Example use:
20221128-210735_Unity-needle

Use nested objects and serialization

You can nest objects and their data. With properly matching @serializable(SomeType) decorators, the data will be serialized and deserialized into the correct types automatically.

In your typescript component:

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();
    }
}

In C# in any script:

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;
}

提示

Without the correct type decorators, you will still get the data, but just as a plain object. This is useful when you're porting components, as you'll have access to all data and can add types as required.

Use Web APIs

提示

Keep in mind that you still have access to all web apis and npm packages!
That's the beauty of Needle Engine if we're allowed to say this here 😊

Display current location

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);
        });
    }
}

Display current time using a Coroutine

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)
        }
    };
}

Change custom shader property

Assuming you have a custom shader with a property name _Speed that is a float value this is how you would change it from a script.
You can find a live example to download in our samples

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

Switching src attribute

See live example on StackBlitz

Adding new postprocessing effects

Make sure to install npm i postprocessing in your web project. Then you can add new effects by deriving from PostProcessingEffect.

To use the effect add it to the same object as your Volume component.

Here is an example that wraps the Outline postprocessing effect. You can expose variables and settings as usual as any effect is also just a component in your three.js scene.

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);

Custom ParticleSystem Behaviour

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())
    }
}

Custom 2D Audio Component

This is an example how you could create your own audio component.
For most usecases however you can use the core AudioSource component and don't have to write code.

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();
        })
    }
}

Arbitrary external files

Use the FileReference type to load external files (e.g. a json file)

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());
    }
}

Receiving html element click in component

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

export class HTMLButtonClick extends Behaviour {

    /** Enter a button query (e.g. button.some-button if you're interested in a button with the class 'some-button') 
     * Or you can also use an id (e.g. #some-button if you're interested in a button with the id 'some-button')
     * Or you can also use a tag (e.g. button if you're interested in any button
    */
    @serializeable()
    htmlSelector: string = "button.some-button";
    
    /** This is the event to be invoked when the html element is clicked. In Unity or Blender you can assign methods to be called in the Editor */
    @serializable(EventList)
    onClick: EventList = new EventList();

    private element? : HTMLButtonElement;

    onEnable() {
        // Get the element from the DOM
        this.element = document.querySelector(this.htmlSelector) as HTMLButtonElement;
        if (this.element) {
            this.element.addEventListener('click', this.onClicked);
        }
        else console.warn(`Could not find element with selector \"${this.htmlSelector}\"`);
    }

    onDisable() {
        if (this.element) {
            this.element.removeEventListener('click', this.onClicked);
        }
    }

    private onClicked = () => {
        this.onClick.invoke();
    }
}
View on GitHub

Disable environment light

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

export class DisableEnvironmentLight extends Behaviour {

   private _previousEnvironmentTexture: Texture | null = null;

   onEnable(): void {
       this._previousEnvironmentTexture = this.context.scene.environment;
       this.context.scene.environment = null;
   }

   onDisable(): void {
       this.context.scene.environment = this._previousEnvironmentTexture;
   }
}
View on GitHub

Use mediapipe package to control the 3D scene with hands

Make sure to install the mediapipe package. Visit the github link below to see the complete project setup.
Try it live here - requires a webcam/camera

import { FilesetResolver, HandLandmarker, HandLandmarkerResult, NormalizedLandmark } from "@mediapipe/tasks-vision";
import { Behaviour, Mathf, serializable, showBalloonMessage } from "@needle-tools/engine";
import { ParticleSphere } from "./ParticleSphere";

export class MediapipeHands extends Behaviour {

    @serializable(ParticleSphere)
    spheres: ParticleSphere[] = [];

    private _video!: HTMLVideoElement;
    private _handLandmarker!: HandLandmarker;

    async awake() {
        showBalloonMessage("Initializing mediapipe...")

        const vision = await FilesetResolver.forVisionTasks(
            // path/to/wasm/root
            "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
        );
        this._handLandmarker = await HandLandmarker.createFromOptions(
            vision,
            {
                baseOptions: {
                    modelAssetPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task",
                    delegate: "GPU"
                },
                numHands: 2
            });
        //@ts-ignore
        await this._handLandmarker.setOptions({ runningMode: "VIDEO" });

        this._video = document.createElement("video");
        this._video.setAttribute("style", "max-width: 30vw; height: auto;");
        console.log(this._video);
        this._video.autoplay = true;
        this._video.playsInline = true;
        this.context.domElement.appendChild(this._video);
        this.startWebcam(this._video);
    }

    private _lastVideoTime: number = 0;

    update(): void {
        if (!this._video || !this._handLandmarker) return;
        const video = this._video;
        if (video.currentTime !== this._lastVideoTime) {
            let startTimeMs = performance.now();
            showBalloonMessage("<strong>Control the spheres with one or two hands</strong>!<br/><br/>Sample scene by <a href='https://twitter.com/llllkatjallll/status/1659280435023605773'>Katja Rempel</a>")
            const detections = this._handLandmarker.detectForVideo(video, startTimeMs);
            this.processResults(detections);
            this._lastVideoTime = video.currentTime;
        }

    }

    private processResults(results: HandLandmarkerResult) {
        const hand1 = results.landmarks[0];
        // check if we have even one hand
        if (!hand1) return;

        if (hand1.length >= 4 && this.spheres[0]) {
            const pos = hand1[4];
            this.processLandmark(this.spheres[0], pos);
        }

        // if we have a second sphere:
        if (this.spheres.length >= 2) {
            const hand2 = results.landmarks[1];
            if (!hand2) {
                const pos = hand1[8];
                this.processLandmark(this.spheres[1], pos);
            }
            else {
                const pos = hand2[4];
                this.processLandmark(this.spheres[1], pos);
            }
        }
    }

    private processLandmark(sphere: ParticleSphere, pos: NormalizedLandmark) {
        const px = Mathf.remap(pos.x, 0, 1, -6, 6);
        const py = Mathf.remap(pos.y, 0, 1, 6, -6);
        sphere.setTarget(px, py, 0);
    }

    private async startWebcam(video: HTMLVideoElement) {
        const constraints = { video: true, audio: false };
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
    }
}
View on GitHub

Change Color On Collision

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

export class ChangeColorOnCollision extends Behaviour {

    private renderer: Renderer | null = null;
    private collisionCount: number = 0;

    private _startColor? : Color[];

    start() {
        this.renderer = this.gameObject.getComponent(Renderer);
        if (!this.renderer) return;
        if(!this._startColor) this._startColor = [];
        for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
            this.renderer.sharedMaterials[i] = this.renderer.sharedMaterials[i].clone();
            this._startColor[i] = this.renderer.sharedMaterials[i]["color"].clone();
        }
    }

    onCollisionEnter(_col: Collision) {
        if (!this.renderer) return;
        this.collisionCount += 1;
        for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
            this.renderer.sharedMaterials[i]["color"].setRGB(Math.random(), Math.random(), Math.random());
        }
    }

    onCollisionExit(_col: Collision) {
        if (!this.renderer || !this._startColor) return;
        this.collisionCount -= 1;
        if (this.collisionCount === 0) {
            for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
                this.renderer.sharedMaterials[i]["color"].copy(this._startColor[i])
                // .setRGB(.1, .1, .1);
            }
        }
    }

    // more events:
    // onCollisionStay(_col: Collision)
    // onCollisionExit(_col: Collision)
}
View on GitHub

Physics Trigger Relay

Invoke events using an objects physics trigger methods

export class PhysicsTrigger extends Behaviour {

    @serializeable(GameObject)
    triggerObjects?:GameObject[];

    @serializeable(EventList)
    onEnter?: EventList;

    @serializeable(EventList)
    onStay?: EventList;

    @serializeable(EventList)
    onExit?: EventList;

    onTriggerEnter(col: Collider) {
        if(this.triggerObjects && this.triggerObjects.length > 0 && !this.triggerObjects?.includes(col.gameObject)) return;
        this.onEnter?.invoke();
    }

    onTriggerStay(col: Collider) {
        if(this.triggerObjects && this.triggerObjects.length > 0 && !this.triggerObjects?.includes(col.gameObject)) return;
        this.onStay?.invoke();
    }

    onTriggerExit(col: Collider) {
        if(this.triggerObjects && this.triggerObjects.length > 0 && !this.triggerObjects?.includes(col.gameObject)) return;
        this.onExit?.invoke();
    }
}
View on GitHub

Auto Reset

Reset an object's position automatically when it's leaving a physics trigger

import { Behaviour, Collider, GameObject, Rigidbody, serializeable } from "@needle-tools/engine";
import { Vector3 } from "three";

export class StartPosition extends Behaviour {

    //@nonSerialized
    startPosition?: Vector3;

    start() {
        this.updateStartPosition();
    }

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

    resetToStart() {
        if (!this.startPosition) return;
        const rb = GameObject.getComponent(this.gameObject, Rigidbody);
        rb?.teleport(this.startPosition);
    }
}

/** Reset to start position when object is exiting the collider */
export class AutoReset extends StartPosition {

    @serializeable(Collider)
    worldCollider?: Collider;

    start(){
        super.start();
        if(!this.worldCollider) console.warn("Missing collider to reset", this);
    }
    
    onTriggerExit(col) {
        if(col === this.worldCollider){
            this.resetToStart();
        }
    }
}
View on GitHub

Play Audio On Collision

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

export class PlayAudioOnCollision extends Behaviour {
    @serializeable(AudioSource)
    audioSource?: AudioSource;

    onCollisionEnter() {
        this.audioSource?.play();
    }
}
View on GitHub

Set Random Color

Randomize the color of an object on start. Note that the materials are cloned in the start method

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

export class RandomColor extends Behaviour {

    @serializeable()
    applyOnStart: boolean = true;

    start() {
        if (this.applyOnStart)
            this.applyRandomColor();

        // if materials are not cloned and we change the color they might also change on other objects
        const cloneMaterials = true;
        if (cloneMaterials) {
            const renderer = this.gameObject.getComponent(Renderer);
            if (!renderer) {
                return;
            }
            for (let i = 0; i < renderer.sharedMaterials.length; i++) {
                renderer.sharedMaterials[i] = renderer.sharedMaterials[i].clone();
            }
        }
    }

    applyRandomColor() {
        const renderer = this.gameObject.getComponent(Renderer);
        if (!renderer) {
            console.warn("Can not change color: No renderer on " + this.name);
            return;
        }
        for (let i = 0; i < renderer.sharedMaterials.length; i++) {
            renderer.sharedMaterials[i].color = new Color(Math.random(), Math.random(), Math.random());
        }
    }
}
View on GitHub

Spawn Objects Over Time

import { Behaviour, GameObject, LogType, serializeable, showBalloonMessage, WaitForSeconds } from "@needle-tools/engine";

export class TimedSpawn extends Behaviour {
    @serializeable(GameObject)
    object?: GameObject;

    interval: number = 1000;
    max: number = 100;

    private spawned: number = 0;

    awake() {
        if (!this.object) {
            console.warn("TimedSpawn: no object to spawn");
            showBalloonMessage("TimedSpawn: no object to spawn", LogType.Warn);
            return;
        }
        GameObject.setActive(this.object, false);
        this.startCoroutine(this.spawn())
    }

    *spawn() {
        if (!this.object) return;
        while (this.spawned < this.max) {
            const instance = GameObject.instantiate(this.object);
            GameObject.setActive(instance!, true);
            this.spawned += 1;
            yield WaitForSeconds(this.interval / 1000);
        }
    }
}
View on GitHub

页面由 AI 自动翻译

Suggest changes
最近更新:: 2025/4/22 08:44
Prev
自动生成组件
Next
Community Contributions