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
    • 适用于 Blender 的 Needle Engine
    • Needle Engine 作为 Web Component
    • 在您的网站上使用 Needle Engine
    • Needle Cloud
  • Topics

    • Web 项目结构
    • Everywhere Actions
    • Exporting Assets to glTF
    • 框架、打包器、HTML
    • 在本地设备上测试
    • 部署与优化
  • Advanced

    • 网络
    • VR 和 AR (WebXR)
    • /lang/zh/vanilla-js.html
    • 编辑器同步
  • Troubleshooting

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

    • Tutorials on Youtube
    • Interviews on Youtube
  • Scripting Overview

    • 在 Needle Engine 中编写脚本
    • 面向 Unity 开发者的脚本编写简介
    • Needle 核心组件
    • Everywhere Actions
  • Components and Lifecycle

    • Creating and using Components
    • @serializable 和其他装饰器
    • 自动生成组件
    • 脚本示例
    • 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
    • 适用于 Blender 的 Needle Engine
    • Needle Engine 作为 Web Component
    • 在您的网站上使用 Needle Engine
    • Needle Cloud
  • Topics

    • Web 项目结构
    • Everywhere Actions
    • Exporting Assets to glTF
    • 框架、打包器、HTML
    • 在本地设备上测试
    • 部署与优化
  • Advanced

    • 网络
    • VR 和 AR (WebXR)
    • /lang/zh/vanilla-js.html
    • 编辑器同步
  • Troubleshooting

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

    • Tutorials on Youtube
    • Interviews on Youtube
  • Scripting Overview

    • 在 Needle Engine 中编写脚本
    • 面向 Unity 开发者的脚本编写简介
    • Needle 核心组件
    • Everywhere Actions
  • Components and Lifecycle

    • Creating and using Components
    • @serializable 和其他装饰器
    • 自动生成组件
    • 脚本示例
    • 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
    • 在本地设备上测试
    • 部署与优化
    • 如何调试
    • 常见问题 (FAQ) 💡
  • Scripting

    • 在 Needle Engine 中编写脚本
    • 面向 Unity 开发者的脚本编写简介
    • Creating and using Components
    • 自动生成组件
    • 脚本示例
    • Community Contributions
  • Advanced

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

    • 功能概览
    • 技术概述
    • Needle 核心组件
    • needle.config.json
    • <needle-engine> 配置
    • @serializable 和其他装饰器

脚本示例

如果您刚接触脚本,我们强烈建议先阅读以下指南:

  • 新手指南:Typescript Essentials
  • 新手指南:面向 Unity 开发者的 Needle Engine
  • 视频教程:如何编写自定义组件

下面是一些基本脚本作为快速参考。

我们还提供了许多示例场景和完整项目,您可以下载并作为起点:

  • 访问示例网站
  • 下载示例包
  • Needle Engine Stackblitz 集合
  • Needle Engine API

基本组件

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

参见 脚本 以了解所有组件事件

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

从 Unity 引用和加载资源 (Prefab 或 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
    }  
}

从 Unity 引用和加载场景

提示

在我们的示例中找到一个工作示例以下载和尝试

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

接收对象的点击

将此脚本添加到场景中任何您希望可点击的对象上。确保该对象的父级层次结构中也有一个 ObjectRaycaster 组件。

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

对象的网络化点击

将此脚本添加到场景中任何您希望可点击的对象上。确保该对象的父级层次结构中也有一个 ObjectRaycaster 组件。 该组件会将接收到的点击发送给所有连接的客户端,并触发一个事件,您可以在应用程序中对该事件作出反应。如果您使用 Unity 或 Blender,您可以简单地将函数分配给 onClick 事件,例如播放动画或隐藏对象。

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

点击时播放动画

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

引用一个 Animation Clip

如果您想运行自定义动画逻辑,这会很有用。 您也可以导出一个 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);
    }
}

创建并调用 UnityEvent

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

export class MyComponent extends Behaviour {

    @serializable(EventList)
    myEvent? : EventList;

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

提示

EventList 事件也会在组件级别触发。这意味着您也可以使用 myComponent.addEventListener("my-event", evt => {...}) 订阅上面声明的事件。 这是一项实验性功能。请在我们的论坛中提供反馈

声明一个自定义事件类型

这在您希望向 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);
    }
}

示例用法:20221128-210735_Unity-needle

使用嵌套对象和序列化

您可以嵌套对象及其数据。使用正确匹配的 @serializable(SomeType) 装饰器,数据将自动序列化和反序列化为正确的类型。

在您的 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# 脚本中:

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

提示

如果没有正确的类型装饰器,您仍然会获得数据,但只是一个普通对象。这在移植组件时非常有用,因为您可以访问所有数据并根据需要添加类型。

使用 Web API

提示

请记住,您仍然可以访问所有 web apis 和 npm 包! 如果允许我们在这里这样说,这就是 Needle Engine 的魅力所在 😊

显示当前位置

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

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

改变自定义着色器属性

假设您有一个自定义着色器,其属性名称为 _Speed,是一个浮点值,您可以通过脚本来改变它。 您可以在我们的示例中找到一个可下载的实时示例。

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

切换 src 属性

请参阅 StackBlitz 上的实时示例。

添加新的后处理效果

请确保在您的 web 项目中安装 npm i postprocessing。然后,您可以派生自 PostProcessingEffect 来添加新的效果。

要使用该效果,将其添加到与您的 Volume 组件相同的对象上。

这里有一个封装了 Outline 后处理效果的示例。您可以像往常一样公开变量和设置,因为任何效果在您的 three.js 场景中也只是一个组件。

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

自定义 ParticleSystem 行为

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

自定义 2D Audio 组件

这是一个如何创建自己的音频组件的示例。 然而,对于大多数用例,您可以使用核心 AudioSource 组件,无需编写代码。

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

任意外部文件

使用 FileReference 类型加载外部文件(例如 json 文件)

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

在组件中接收 HTML 元素的点击

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

禁用环境光

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

使用 mediapipe 包用手控制 3D 场景

确保安装 mediapipe 包。访问下面的 github 链接以查看完整的项目设置。 在这里实时尝试 - 需要摄像头/相机

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

碰撞时改变颜色

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

物理触发器中继

使用对象的物理触发器方法触发事件

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

自动重置

当对象离开物理触发器时自动重置其位置

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

碰撞时播放音频

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

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

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

设置随机颜色

在开始时随机化对象的颜色。注意,材质在 start 方法中会被克隆

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

按时间间隔生成对象

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/6/11 12:25
Prev
自动生成组件
Next
Community Contributions