Exemplos de Scripting
Se é novo em scripting, altamente recomendamos ler os guias seguintes primeiro:
- Guia para Iniciantes: Essenciais de Typescript
- Guia para Iniciantes: Needle Engine para Developers Unity
- Tutorial em vídeo: Como escrever componentes personalizados
Abaixo encontrará alguns scripts básicos como referência rápida.
Também oferecemos muitas cenas de exemplo e projetos completos que pode descarregar e usar como ponto de partida:
- Visitar Website de Exemplos
- Descarregar Pacote de Exemplos
- Coleção Stackblitz do Needle Engine
- API do Needle Engine
Componente básico
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);
}
}
ver scripting para todos os eventos de componente
Referenciar um Objeto do 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;
}
Referenciar e carregar um asset do Unity (Prefab ou 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
}
}
Referenciar e carregar cenas do Unity
Tips
Encontre um exemplo funcional nos nossos samples para descarregar e experimentar
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();
}
}
}
Receber Cliques em Objetos
Adicione este script a qualquer objeto na sua cena que deseja que seja clicável. Certifique-se de ter também um componente ObjectRaycaster
na hierarquia pai desse objeto.
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);
}
}
Cliques em Objetos em Rede (Networking)
Adicione este script a qualquer objeto na sua cena que deseja que seja clicável. Certifique-se de ter também um componente ObjectRaycaster
na hierarquia pai desse objeto. O componente enviará o clique recebido para todos os clientes conectados e levantará um evento ao qual pode reagir na sua aplicação. Se estiver a usar Unity ou Blender, pode simplesmente atribuir funções para chamar ao evento onClick
para, por exemplo, reproduzir uma animação ou ocultar objetos.
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();
}
}
Reproduzir Animação ao Clique
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();
}
}
}
Referenciar um Animation Clip
Isto pode ser útil se deseja executar a sua lógica de animação personalizada. Pode também exportar um array de 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);
}
}
Criar e invocar um UnityEvent
import { Behaviour, serializable, EventList } from "@needle-tools/engine"
export class MyComponent extends Behaviour {
@serializable(EventList)
myEvent? : EventList;
start() {
this.myEvent?.invoke();
}
}
Tips
Os eventos EventList são também invocados ao nível do componente. Isto significa que também pode subscrever o evento declarado acima usando myComponent.addEventListener("my-event", evt => {...})
também. Esta é uma funcionalidade experimental. Por favor, forneça feedback no nosso fórum
Declarar um tipo de evento personalizado
Isto é útil quando deseja expor um evento ao Unity ou Blender com alguns argumentos personalizados (como uma 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);
}
}
Exemplo de uso:
Usar objetos aninhados e serialização
Pode aninhar objetos e os seus dados. Com decoradores @serializable(SomeType)
que correspondam corretamente, os dados serão serializados e desserializados nos tipos corretos automaticamente.
No seu componente 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();
}
}
Em C# em qualquer 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;
}
Tips
Sem os decoradores de tipo corretos, ainda obterá os dados, mas apenas como um objeto simples. Isto é útil quando está a portar componentes, uma vez que terá acesso a todos os dados e poderá adicionar tipos conforme necessário.
Usar Web APIs
Tips
Tenha em mente que ainda tem acesso a todas as web apis e pacotes npm! Essa é a beleza do Needle Engine, se nos permitem dizê-lo aqui 😊
Exibir localização atual
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);
});
}
}
Exibir hora atual usando uma 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)
}
};
}
Alterar propriedade de shader personalizada
Assumindo que tem um shader personalizado com um nome de propriedade _Speed
que é um valor float, é assim que o alteraria a partir de um script. Pode encontrar um exemplo live para descarregar nos nossos 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)
}
}
}
Alternar atributo src
Ver exemplo live no StackBlitz
Adicionar novos efeitos de postprocessing
Certifique-se de instalar npm i postprocessing
no seu projeto web. Então pode adicionar novos efeitos derivando de PostProcessingEffect
.
Para usar o efeito, adicione-o ao mesmo objeto que o seu componente Volume
.
Aqui está um exemplo que envolve o efeito de postprocessing Outline. Pode expor variáveis e configurações como de costume, pois qualquer efeito é também apenas um componente na sua cena 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);
Comportamento de ParticleSystem personalizado
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())
}
}
Componente de Áudio 2D personalizado
Este é um exemplo de como poderia criar o seu próprio componente de áudio. Para a maioria dos casos de uso, no entanto, pode usar o componente AudioSource principal e não precisa de escrever código.
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();
})
}
}
Ficheiros externos arbitrários
Use o tipo FileReference para carregar ficheiros externos (por exemplo, um ficheiro 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());
}
}
Receber clique de elemento html em componente
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();
}
}
Desativar luz de ambiente
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;
}
}
Usar pacote mediapipe para controlar a cena 3D com as mãos
Certifique-se de instalar o pacote mediapipe. Visite o link do github abaixo para ver a configuração completa do projeto. Experimente live aqui - requer uma webcam/câmara
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;
}
}
Alterar Cor na Colisão
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)
}
Relay de Trigger Físico
Invocar eventos usando métodos de trigger físico de um objeto
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();
}
}
Reset Automático
Resetar a posição de um objeto automaticamente quando este está a sair de um trigger físico
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();
}
}
}
Reproduzir Áudio na Colisão
import { AudioSource, Behaviour, serializeable } from "@needle-tools/engine";
export class PlayAudioOnCollision extends Behaviour {
@serializeable(AudioSource)
audioSource?: AudioSource;
onCollisionEnter() {
this.audioSource?.play();
}
}
Definir Cor Aleatória
Aleatorizar a cor de um objeto ao iniciar. Note que os materiais são clonados no método 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());
}
}
}
Spawn de Objetos ao Longo do Tempo
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);
}
}
}
Página traduzida automaticamente usando IA