Introduction

Runtime code for Needle Engine is written in TypeScriptopen in new window (recommended) or JavaScriptopen in new window. We automatically generate C# stub components out of that, which you can add to GameObjects in the editor. The C# components and their data are recreated by the runtime as JavaScript components with the same data and attached to three.js objects.

Both custom components as well as built-in Unity components can be mapped to JavaScript components in this way. For example, mappings for many built-in components related to animation, rendering or physics are already included in Needle Engine.


Our JavaScript runtime API adopts a component model similar to the Unity Editor, and provides a lot of functionality that will feel familiar to Unity devs.
JavaScript components attached to three.js objectsopen in new window have lifecycle methods, like awake, start, onEnable, onDisable, update and lateUpdate, that you can implement. You can also use Coroutines.

Learn about Needle Engine lifecycle in the section about Lifecycle Methods below.
Learn more about the Unity lifecycle hereopen in new window.

To get an in-depth overview of built-in components, you can inspect the folder Packages/Needle Engine Exporter/Core/Runtime/Components in the Project Windowopen in new window.

Needle Engine's Exporter does not compile your existing C# code to Web Assembly. While using Web Assembly may result in better performance at runtime, it comes at a high cost for iteration speed and flexibility in building web experiences. Read more about our vision and technical overview.


When you don't need to write code

Often, interactive scenes can be realized using Events in Unity and calling methods on built-in components. A typical example is playing an animation on button click - you create a button, add a Click event in the inspector, and have that call Animator.SetTrigger or similar to play a specific animation.

Needle Engine translates Unity Events into JavaScript method calls, which makes this a very fast and flexible workflow - set up your events as usual and when they're called they'll work the same as in Unity.

image
An example of a Button Click Event that is working out-of-the-box in Needle Engine — no code needed.

The same works for custom components that implement UnityEvent<>. This means that you can create custom components for artists and designers to wire up complex behaviours without writing any code.

If you intend to expose/generate a UnityEvent in a custom component that you work on you can do it like this:

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

export class MyComponent extends Behaviour {

    @serializable(EventList)
    myEvent? : EventList;

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

Creating a new component 📑

Scripts are written in TypeScriptopen in new window (recommended) or JavaScript. There's two ways to add custom scripts to your project:

  • Simply add a .ts or .js file inside src/scripts/ in your generated project directory.
    Generated C# components are placed under Assets/Needle/Components.codegen.

  • Organize your code into NPM Definition Files. These help you to modularize and re-use code between projects.
    Generated C# components are placed in a .codegen folder next to the NpmDef.
    You can create NpmDef files via Create > NPM Definition and then add TypeScript files by right-clicking an NpmDef file and selecting Create > TypeScript. Please see this chapter for more information.

In both approaches, source directories are watched for changes and C# components are regenerated whenever a change is detected. Changes to the source files also result in a hot reload of the running website – you don't have to wait for Unity to recompile the C# components. This makes iterating on code pretty much instant.

Tip: You can have multiple components inside one file.

Example Workflow

  • Create a component that rotates an object
    Create src/scripts/Rotate.ts and add the following code:
import { Behaviour, serializable } from "@needle-tools/engine";

export class Rotate extends Behaviour
{
    @serializable()
    speed : number = 1;

    start(){
        // logging this is useful for debugging in the browser. 
        // You can open the developer console (F12) to see what data your component contains
        console.log(this);
    }

    // update will be called every frame
    update(){
        this.gameObject.rotateY(this.context.time.deltaTime * this.speed);
    }
}

Now inside Unity a new script called Rotate.cs will be automatically generated. Add the script to a Cube that is exported as part of a glTF file (it needs a GltfObject component in its parent) and save the scene. The cube is now rotating inside the browser.
Open the chrome developer console to inspect the log from the Rotate.start method. This is a helpful practice to learn and debug what fields are exported and currently assigned. In general all public and serializable fields and all public properties are exported.

Now add a new field public float speed = 5 to your Unity component and save it. The Rotate component inspector now shows a speed field that you can edit. Save the scene (or click the Build button) and note that the javascript component now has the exported speed value assigned.

Note: It is also possible to ignore, convert or add fields on export in Unity by extending our export process. This is currently undocumented and subject to change.

Function with argument

Please refer to the TypeScriptopen in new window documentation to learn more about the syntax and language.

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

export class PrintNumberComponent extends Behaviour
{
    start(){
      this.printNumber(42);
    }
    
    private printNumber(myNumber : number){
        console.log("My Number is: " + myNumber);
    }
}

Component architecture

Components are added to threejs Object3Dsopen in new window similar to how components in Unityopen in new window are added to GameObjectsopen in new window. Therefore when we want to access a three.js Object3D, we can access it as this.gameObject which returns our Object3D.

Note: Setting visible to false on a Object3D will act like SetActive(false) in Unity - meaning it will also disable all the current components on this object and its children. Update events for inactive components are not being called until visible is set to true again.

Lifecycle methods

  • awake() - First method being called when a new component is created
  • onEnable() - Called when a component is enabled (e.g. when enabled changes from false to true)
  • onDisable() - Called when a component is disabled (e.g. when enabled changes from true to false)
  • onDestroy() - called when the Object3D or component is being destroyed
  • start() - Called on the start of the first frame after the component was created
  • earlyUpdate() - First mainloop update event
  • update() - Regular mainloop update event
  • lateUpdate()
  • onBeforeRender() - Last update event before render call
  • onAfterRender() - Called after render event

Note: It is important to understand that similar to Unity lifecycle methods are only being called when they are declared. So only declare update lifecycle methods when they are actually necessary, otherwise it may hurt performance if you have many components with update loops that do nothing.

Physic event methods

  • onCollisionEnter(col : Collision)
  • onCollisionStay(col : Collision)
  • onCollisionExit(col : Collision)
  • onTriggerEnter(col : Collision)
  • onTriggerStay(col : Collision)
  • onTriggerExit(col : Collision)

Coroutines

Coroutines can be declared using the JavaScript Generator Syntaxopen in new window.
To start a coroutine, call this.startCoroutine(this.myRoutineName());

Example

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

export class Rotate extends Behaviour {

    start() {
        // the second argument is optional and allows you to specifiy 
        // when it should be called in the current frame loop
        // coroutine events are called after regular component events of the same name
        // for example: Update coroutine events are called after component.update() functions
        this.startCoroutine(this.rotate(), FrameEvent.Update);
    }

    // this method is called every frame until the component is disabled
    *rotate() {
        // keep looping forever
        while (true) {
            yield;
        }
    }
}

To stop a coroutine, either exit the routine by returning from it, or cache the return value of startCoroutine and call this.stopCoroutine(<...>). All Coroutines are stopped at onDisable / when disabling a component.

Finding, adding and removing components

To access other components, use the static methods on GameObject or this.gameObject methods. For example, to access a Renderer component in the parent use GameObject.getComponentInParent(this.gameObject, Renderer) or this.gameObject.getComponentInParent(Renderer).

Example:

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

export class MyComponent extends Behaviour {

    start() {
        const renderer = GameObject.getComponentInParent(this.gameObject, Renderer);
        console.log(renderer);
    }
}

Some of the available methods:

  • GameObject.instantiate(Object3D, InstantiateOptions) - creates a new instance of this object including new instances of all its components.
  • GameObject.destroy(Object3D|Component) - destroy a component or Object3D (and its components)
  • GameObject.addNewComponent(Object3D, Type) - adds (and creates) a new component for a type to the provided object. Note that awake and onEnable is already called when the component is returned.
  • GameObject.addComponent(Object3D, Component) - moves a component instance to the provided object.
  • GameObject.removeComponent(Component) - removes a component from a gameObject
  • GameObject.getComponent(Object3D, Type) - returns the first component matching a type on the provided object.
  • GameObject.getComponents(Object3D, Type) - returns all components matching a type on the provided object.
  • GameObject.getComponentInChildren - same as getComponent but also searches in child objects.
  • GameObject.getComponentsInChildren - same as getComponents but also searches in child objects.
  • GameObject.getComponentInParent - same as getComponent but also searches in parent objects.
  • GameObject.getComponentsInParent - same as getComponents but also searches in parent objects.
  • GameObject.findObjectOfType - searches the whole scene for a type.
  • GameObject.findObjectsOfType - searches the whole scene for all matching types.

The Context and the HTML DOM

The context refers to the runtime inside a web componentopen in new window.
The three.js scene lives inside a custom HTML component called <needle-engine> (see the index.html in your project). You can access that element using this.context.domElement.

This architecture allows for potentially having multiple needle WebGL scenes on the same webpage, that can either run on their own or communicate between each other as parts of your webpage.

Note: The exporter currently only supports exporting one scene for one html element, but you can create HTML files with multiple contexts. We might make this easier in the future.

Three.js Scene

Access the three.js sceneopen in new window using this.context.scene.

Time

Use this.context.time to access time, frameCount or deltaTime (time since last frame in milliseconds).

Input

Use this.context.input to access convenient methods for getting mouse and touch data. WebXR controller access is currently separate.

Physics

Use this.context.physics to access the physics API, for example to perform raycasts against scene geometry.

Note: Unity Layersopen in new window are mapped from Unity to three.js Layersopen in new window. By default, physics will ignore objects on layer 2 (this is the Ignore Raycast layer in Unity) but hit all other layers. We recommended setting up your layers as needed in Unity, but if you need, you can override this behaviour using the options parameter that you can pass to the physics.raycast method.

Networking

Networking methods can be accessed via this.context.connection. Please refer to the networking docs for further information.

Assets

Use this.context.assets to get access to assets and resources that are imported inside glTF files.

Accessing URL Parameters

Use utils.getParam(<..>) to quickly access URL parameters and define behaviour with them.

Example:

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

export class MyScript extends Behaviour
{ 
    awake(): void {
        // access the url parameter
        const urlParam = getParam("target");
        if (urlParam && typeof urlParam === "string" && urlParam.length > 0) {
            // const do something based on ?target=some_string
        }
    }
}

Accessing components from external JavaScript

It is possible to access all the functionality described above using regular JavaScript code that is not inside components and lives somewhere else. All the components and functionality of the needle runtime is accessible via the global Needle namespace (you can write console.log(Needle) to get an overview)

For that just find the <needle-engine> web-component in your DOM and retrieve the Context from it e.g. by calling await document.querySelector("needle-engine")?.getContext().

You can find components using Needle.findObjectOfType(Needle.AudioSource) for example. It is recommended to cache those references, as searching the whole scene repeatedly is expensive. See the list for finding adding and removing components above.

For getting callbacks for the initial scene load see the following example:

<needle-engine loadstart="loadingStarted" progress="loadingProgress" loadfinished="loadingFinished"></needle-engine>

<script type="text/javascript">
function loadingStarted() { console.log("START") }
function loadingProgress() { console.log("LOADING...") }
function loadingFinished() { console.log("FINISHED!") }
</script>

Automatically generating Unity components from typescript files

Automatically generate Unity components for typescript component in your project using Needle component compileropen in new window

  • If you want to add scripts inside the src/scripts folder in your project then you need to have a Component Generator on the GameObject with your ExportInfo component.
  • Now when adding new components in your/threejs/project/src/scriptsit will automatically generate Unity scripts in ``Assets/Needle/Components.codegen`.
  • If you want to add scripts to any NpmDef file you can just create them - each NpmDef automatically watches script changes and handles component generation, so you don't need any additional component in your scene.

Note: for C# fields to be correctly generated it is currently important that you explictly declare a Typescript type. For example myField : number = 5

See codegen example and how to extend it 🤘

You can switch between Typescript input and generated C# stub components using the tabs below

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

export class MyCustomComponent extends Behaviour {
    @serializable()
    myFloatValue: number = 42;

    @serializable(Object3D)
    myOtherObject?: Object3D;

    @serializable(AssetReference)
    prefabs: AssetReference[] = [];

    start() {
        this.sayHello();
    }

    private sayHello() {
        console.log("Hello World", this);
    }
}
// NEEDLE_CODEGEN_START
// auto generated code - do not edit directly

#pragma warning disable

namespace Needle.Typescript.GeneratedComponents
{
	public partial class MyCustomComponent : UnityEngine.MonoBehaviour
	{
		public float @myFloatValue = 42f;
		public UnityEngine.Transform @myOtherObject;
		public UnityEngine.Transform[] @prefabs = new UnityEngine.Transform[]{ };
		public void start(){}
		public void update(){}
	}
}

// NEEDLE_CODEGEN_END
using UnityEditor;

// you can add code above or below the NEEDLE_CODEGEN_ blocks

// NEEDLE_CODEGEN_START
// auto generated code - do not edit directly

#pragma warning disable

namespace Needle.Typescript.GeneratedComponents
{
	public partial class MyCustomComponent : UnityEngine.MonoBehaviour
	{
		public float @myFloatValue = 42f;
		public UnityEngine.Transform @myOtherObject;
		public UnityEngine.Transform[] @prefabs = new UnityEngine.Transform[]{ };
		public void start(){}
		public void update(){}
	}
}

// NEEDLE_CODEGEN_END

namespace Needle.Typescript.GeneratedComponents
{
    // This is how you extend the generated component (namespace and class name must match!)
	public partial class MyCustomComponent : UnityEngine.MonoBehaviour
	{
		
		public void MyAdditionalMethod()
		{
		}

		private void OnValidate()
		{
			myFloatValue = 42;
		}
	}

    // of course you can also add custom editors
	[CustomEditor(typeof(MyCustomComponent))]
	public class MyCustomComponentEditor : Editor
	{
		public override void OnInspectorGUI()
		{
			EditorGUILayout.HelpBox("This is my sample component", MessageType.None);
			base.OnInspectorGUI();
		}
	}
}

Controlling component generation

You can use the following typescript attributes to control C# code generation behavior:

AttributeResult
// @generate-componentForce generation of next class
// @dont-generate-componentDisable generation of next class
// @serializeFieldDecorate generated field with [SerializeField]
// @type UnityEngine.CameraSpecify generated C# field type
// @nonSerializedSkip generating the next field or method

The attribute @dont-generate-component is especially useful if you have an existing Unity script you want to match. You'll have to ensure yourself that the serialized fields match in this case – only matching fields/properties will be exported.

Note: exported members will start with a lowercase letter. For example if your C# member is named MyString it will be assigned to myString.

Extending generated components

Component C# classes are generated with the partialopen in new window flag so that it is easy to extend them with functionality. This is helpful to draw gizmos, add context menus or add additional fields or methods that are not part of a built-in component.

Version Control

While generated C# components use the type name to produce stable GUIDs, we recommend checking in generated components in version control as a good practice.

Serialization / Components in glTF files

To embed components and recreate components with their correct types in glTF, we also need to save non-primitive types (everything that is not a Number, Boolean or String). You can do so is adding a @serializable(<type>) decorator above your field or property.

Example:

import { Behaviour, serializable } 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;
} 

To serialize from and to custom formats, it is possible to extend from the TypeSerializer class and create an instance. Use super() in the constructor to register supported types.

Note: In addition to matching fields, matching properties will also be exported when they match to fields in the typescript file.

AssetReference and Addressables

Referenced Prefabs, SceneAssets and AssetReferencesopen in new window in Unity will automatically be exported as glTF files (please refer to the Export Prefabs documentation).

These exported gltf files will be serialized as plain string URIs. To simplify loading these from TypeScript components, we added the concept of AssetReference types. They can be loaded at runtime and thus allow to defer loading parts of your app or loading external content.

Example:

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

AssetReferences are cached by URI, so if you reference the same exported glTF/Prefab in multiple components/scripts it will only be loaded once and then re-used.

Renamed Unity Types in TypeScript

For future compatibility, some Unity-specific types are mapped to different type names in our engine.

Unity TypeType in Needle EngineDescription
UnityEventEventList
TransformObject3D
TransformAssetReferencewhen assigning a prefab or scene asset in Unity
floatnumber
ColorRGBAColor

For Unity Devs

If you are a Unity dev and want to learn more about typescript and Needle Engine you can also learn more in Needle Engine for Unity developers 😊