Needle Engine provides a tight integration into the Unity Editor. This allows developers and designers alike to work together in a familiar environment and deliver fast, performant and lightweight web-experiences.
The following guide is mainly aimed at developers with a Unity3D background but it may also be useful for developers with a web or three.js background. It covers topics regarding how things are done in Unity vs in three.js or Needle Engine.
If you are all new to Typescript and Javascript and you want to dive into writing scripts for Needle Engine then we also recommend reading the Typescript Essentials Guide for a basic understanding between the differences between C# and Javascript/Typescript.
If you want to code-along you can open engine.needle.tools/new to create a small project that you can edit in the browser ⚡
The Basics
Needle Engine is a 3d web engine running on-top of three.js. Three.js is one of the most popular 3D webgl based rendering libraries for the web. Whenever we refer to a gameObject
in Needle Engine we are actually also talking about a three.js Object3D
, the base type of any object in three.js. Both terms can be used interchangeably. Any gameObject
is a Object3D
.
This also means that - if you are already familiar with three.js - you will have no problem at all using Needle Engine. Everything you can do with three.js can be done in Needle Engine as well. If you are already using certain libraries then you will be able to also use them in a Needle Engine based environment.
Note: 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.
How to create a new Unity project with Needle Engine? (Video)
Creating a Component
In Unity you create a new component by deriving from MonoBehaviour
:
using UnityEngine;
public class MyComponent : MonoBehaviour {
}
A custom component in Needle Engine on the other hand is written as follows:
import { Behaviour } from "@needle-tools/engine"
export class MyComponent extends Behaviour {
}
Script Fields
serializable
If you have seen some Needle Engine scripts then you might have noticed that some variables are annotated with @serializable
above their declaration. This is a Decorator in Typescript and can be used to modify or annotate code. In Needle Engine this is used for example to let the core serialization know which types we expect in our script when it converts from the raw component information stored in the glTF to a Component instance.
Consider the following example:
import { Behaviour, serializable } from "@needle-tools/engine";
import { Object3D } from "three";
class SomeClass extends Behaviour{
@serializable(Behaviour)
myOtherComponent?: Behaviour;
@serializable(Object3D)
someOtherObject?: Object3D;
}
This tells Needle Engine that myOtherComponent
should be of type Behaviour
. It will then automatically assign the correct reference to the field when your scene is loaded. The same is true for someOtherObject
where we want to deserialize to an Object3D
reference.
Note that in some cases the type can be ommitted. This can be done for all primitive types in Javascript. These are boolean
, number
, bigint
, string
, null
and undefined
.
import { Behaviour, serializable } from "@needle-tools/engine";
class SomeClass {
@serializable() // < no type is needed here because the field type is a primitive
myString?: string;
}
public vs private
Field without any accessor modified like private
, public
or protected
will by default be public
in javascript
import { Behaviour, serializable } from "@needle-tools/engine";
class SomeClass {
/// no accessor means it is public:
myNumber?: number;
// explicitly making it private:
private myPrivateNumber?: number;
protected myProtectedNumber?: number;
}
The same is true for methods as well.
GameObjects and the Scene
To access the current scene from a component you use this.scene
which is equivalent to this.context.scene
, this gives you the root three.js scene object.
To traverse the hierarchy from a component you can either iterate over the children of an object
with a for loop:
for (let i = 0; i < this.gameObject.children; i++) {
console.log(this.gameObject.children[i]);
}
or you can iterate using the foreach
equivalent:
for (const child of this.gameObject.children) {
console.log(child);
}
You can also use three.js specific methods to quickly iterate all objects recursively using the traverse
method:
import { GameObject } from "@needle-tools/engine";
//---cut-before---
this.gameObject.traverse((obj: GameObject) => console.log(obj))
or to just traverse visible objects use traverseVisible
instead.
Another option that is quite useful when you just want to iterate objects being renderable you can query all renderer components and iterate over them like so:
import { Renderer } from "@needle-tools/engine";
for(const renderer of this.gameObject.getComponentsInChildren(Renderer))
console.log(renderer);
For more information about getting components see the next section.
Components
Needle Engine is making heavy use of a Component System that is similar to that of Unity. This means that you can add or remove components to any Object3D
/ GameObject
in the scene. A component will be registered to the engine when using addNewComponent(<Object3D>, <ComponentType>)
.
The event methods that the attached component will then automatically be called by the engine (e.g. update
or onBeforeRender
). A full list of event methods can be found in the scripting documentation
Finding Components in the Scene
For getting component you can use the familiar methods similar to Unity. Note that the following uses the Animator
type as an example but you can as well use any component type that is either built-in or created by you.
Method name | Desciption |
---|---|
this.gameObject.getComponent(Animator) | Get the Animator component on a GameObject/Object3D. It will either return the Animator instance if it has an Animator component or null if the object has no such componnent. |
this.gameObject.getComponentInChildren(Animator) | Get the first Animator component on a GameObject/Object3D or on any of its children |
this.gameObject.getComponentsInParents(Animator) | Get all animator components in the parent hierarchy (including the current GameObject/Object3D) |
These methods are also available on the static GameObject type. For example GameObject.getComponent(this.gameObject, Animator)
to get the Animator
component on a passed in GameObject/Object3D.
To search the whole scene for one or multiple components you can use GameObject.findObjectOfType(Animator)
or GameObject.findObjectsOfType(Animator)
.
Renamed Unity Types
Some Unity-specific types are mapped to different type names in our engine. See the following list:
Type in Unity | Type in Needle Engine | |
---|---|---|
UnityEvent | EventList | A UnityEvent will be exported as a EventList type (use serializable(EventList) to deserialize UnityEvents) |
GameObject | Object3D | |
Transform | Object3D | In three.js and Needle Engine a GameObject and a Transform are the same (there is no Transform component). The only exception to that rule is when referencing a RectTransform which is a component in Needle Engine as well. |
Color | RGBAColor | The three.js color type doesnt have a alpha property. Because of that all Color types exported from Unity will be exported as RGBAColor which is a custom Needle Engine type |
Transform
Transform data can be accessed on the GameObject
/ Object3D
directly. Unlike to Unity there is no extra transform component that holds this data.
this.gameObject.position
is the position in local spacethis.gameObject.rotation
is the rotation in euler angles in local spacethis.gameObject.quaternion
- is the quaternion in local spacethis.gameObject.scale
- is the scale in local space
The major difference here to keep in mind is that position
in three.js is by default a localspace position whereas in Unity position
would be worldspace. The next section will explain how to get the worldspace position in three.js.
WORLD- Position, Rotation, Scale...
In three.js (and thus also in Needle Engine) the object.position
, object.rotation
, object.scale
are all local space coordinates. This is different to Unity where we are used to position
being worldspace and using localPosition
to deliberately use the local space position.
If you want to access the world coordinates in Needle Engine we have utility methods that you can use with your objects. Call getWorldPosition(yourObject)
to calculate the world position. Similar methods exist for rotation/quaternion and scale. To get access to those methods just import them from Needle Engine like so import { getWorldPosition } from "@needle.tools/engine"
Note that these utility methods like getWorldPosition
, getWorldRotation
, getWorldScale
internally have a buffer of Vector3 instances and are meant to be used locally only. This means that you should not cache them in your component, otherwise your cached value will eventually be overriden. But it is safe to call getWorldPosition
multiple times in your function to make calculations without having to worry to re-use the same instance. If you are not sure what this means you should take a look at the Primitive Types section in the Typescript Essentials Guide
Time
Use this.context.time
to get access to time data:
this.context.time.time
is the time since the application started runningthis.context.time.deltaTime
is the time that has passed since the last framethis.context.time.frameCount
is the number of frames that have passed since the application startedthis.context.time.realtimeSinceStartup
is the unscaled time since the application has started running
It is also possible to use this.context.time.timeScale
to deliberately slow down time for e.g. slow motion effects.
Raycasting
Use this.context.physics.raycast()
to perform a raycast and get a list of intersections. If you dont pass in any options the raycast is performed from the mouse position (or first touch position) in screenspace using the currently active mainCamera
. You can also pass in a RaycastOptions
object that has various settings like maxDistance
, the camera to be used or the layers to be tested against.
Use this.context.physics.raycastFromRay(your_ray)
to perform a raycast using a three.js ray
Note that the calls above are by default raycasting against visible scene objects. That is different to Unity where you always need colliders to hit objects. The default three.js solution has both pros and cons where one major con is that it can perform quite slow depending on your scene geometry. It may be especially slow when raycasting against skinned meshes. It is therefor recommended to usually set objects with SkinnedMeshRenderers in Unity to the Ignore Raycast
layer which will then be ignored by default by Needle Engine as well.
Another option is to use the physics raycast methods which will only return hits with colliders in the scene.
const hit = this.context.physics.engine?.raycast();
Here is a editable example for physics raycast
Input
Use this.context.input
to poll input state:
import { Behaviour } from "@needle-tools/engine";
export class MyScript extends Behaviour
{
update(){
if(this.context.input.getPointerDown(0)){
console.log("POINTER DOWN")
}
}
}
You can also subscribe to events in the InputEvents
enum like so:
import { Behaviour, InputEvents, NEPointerEvent } from "@needle-tools/engine";
export class MyScript extends Behaviour
{
onEnable(){
this.context.input.addEventListener(InputEvents.PointerDown, this.inputPointerDown);
}
onDisable() {
// it is recommended to also unsubscribe from events when your component becomes inactive
this.context.input.removeEventListener(InputEvents.PointerDown, this.inputPointerDown);
}
inputPointerDown = (evt: NEPointerEvent) => { console.log(evt); }
}
If you want to handle inputs yourself you can also subscribe to all events the browser provides (there are a ton). For example to subscribe to the browsers click event you can write:
window.addEventListener("click", () => { console.log("MOUSE CLICK"); });
Note that in this case you have to handle all cases yourself. For example you may need to use different events if your user is visiting your website on desktop vs mobile vs a VR device. These cases are automatically handled by the Needle Engine input events (e.g. PointerDown
is raised both for mouse down, touch down and in case of VR on controller button down).
InputSystem Callbacks
Similar to Unity (see IPointerClickHandler in Unity) you can also register to receive input events on the component itself.
To make this work make sure your object has a ObjectRaycaster
or GraphicRaycaster
component in the parent hierarchy.
import { Behaviour, IPointerEventHandler, PointerEventData } from "@needle-tools/engine";
export class ReceiveClickEvent extends Behaviour implements IPointerEventHandler {
onPointerClick(args: PointerEventData) {
console.log("Click", args);
}
}
Note: IPointerEventHandler
subscribes the object to all possible pointer events. The handlers for them are:
onPointerDown
onPointerUp
onPointerEnter
onPointerMove
onPointerExit
onPointerClick
All have a PointerEventData
argument describing the event.
Debug.Log
The Debug.Log()
equivalent in javascript is console.log()
. You can also use console.warn()
or console.error()
.
import { GameObject, Renderer } from "@needle-tools/engine";
const someVariable = 42;
// ---cut-before---
console.log("Hello web");
// You can pass in as many arguments as you want like so:
console.log("Hello", someVariable, GameObject.findObjectOfType(Renderer), this.context);
Gizmos
In Unity you normally have to use special methods to draw Gizmos like OnDrawGizmos
or OnDrawGizmosSelected
. In Needle Engine on the other hand such methods dont exist and you are free to draw gizmos from anywhere in your script. Note that it is also your responsibility then to not draw them in e.g. your deployed web application (you can just filter them by if(isDevEnvironment))
).
Here is an example to draw a red wire sphere for one second for e.g. visualizing a point in worldspace
import { Vector3 } from "three";
const hit = { point: new Vector3(0, 0, 0) };
// ---cut-before---
import { Gizmos } from "@needle-tools/engine";
Gizmos.DrawWireSphere(hit.point, 0.05, 0xff0000, 1);
Here are some of the available gizmo methods:
Method name | |
---|---|
Gizmos.DrawArrow | |
Gizmos.DrawBox | |
Gizmos.DrawBox3 | |
Gizmos.DrawDirection | |
Gizmos.DrawLine | |
Gizmos.DrawRay | |
Gizmos.DrawRay | |
Gizmos.DrawSphere | |
Gizmos.DrawWireSphere |
Useful Utility Methods
Import from @needle-tools/engine
e.g. import { getParam } from "@needle-tools/engine"
Method name | Description |
---|---|
getParam() | Checks if a url parameter exists. Returns true if it exists but has no value (e.g. ?help ), false if it is not found in the url or is set to 0 (e.g. ?help=0 ), otherwise it returns the value (e.g. ?message=test ) |
isMobileDevice() | Returns true if the app is accessed from a mobile device |
isDevEnvironment() | Returns true if the current app is running on a local server |
isMozillaXR() | |
isiOS | |
isSafari |
import { isMobileDevice } from "@needle-tools/engine"
if( isMobileDevice() )
import { getParam } from "@needle-tools/engine"
// returns true
const myFlag = getParam("some_flag")
console.log(myFlag)
The Web project
In C# you usually work with a solution containing one or many projects. In Unity this solution is managed by Unity for you and when you open a C# script it opens the project and shows you the file.
You usually install Packages using Unity's built-in package manager to add features provided by either Unity or other developers (either on your team or e.g. via Unity's AssetStore). Unity does a great job of making adding and managing packages easy with their PackageManager and you might never have had to manually edit a file like the manifest.json
(this is what Unity uses to track which packages are installed) or run a command from the command line to install a package.
In a web environment you use npm
- the Node Package Manager - to manage dependencies / packages for you. It does basically the same to what Unity's PackageManager does - it installs (downloads) packages from some server (you hear it usually called a registry in that context) and puts them inside a folder named node_modules
.
When working with a web project most of you dependencies are installed from npmjs.com. It is the most popular package registry out there for web projects.
Here is an example of how a package.json might look like:
{
"name": "@optional_org/package_name",
"version": "1.0.0",
"scripts": {
"start": "vite --host"
},
"dependencies": {
"@needle-tools/engine": "^3.5.9-beta",
"three": "npm:@needle-tools/[email protected]"
},
"devDependencies": {
"@types/three": "0.146.0",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"typescript": "^5.0.4",
"vite": "^4.3.4",
"vite-plugin-compression": "^0.5.1"
}
}
Our default template uses Vite as its bundler and has no frontend framework pre-installed. Needle Engine is unoppionated about which framework to use so you are free to work with whatever framework you like. We have samples for popular frameworks like Vue.js, Svelte, Next.js, React or React Three Fiber.
Installing packages & dependencies
To install a dependency from npm you can open your web project in a commandline (or terminal) and run npm i <the/package_name>
(shorthand for npm install
)
For example run npm i @needle-tools/engine
to install Needle Engine. This will then add the package to your package.json
to the dependencies
array.
To install a package as a devDependency only you can run npm i --save-dev <package_name>
. More about the difference between dependencies and devDependencies below.
What's the difference between 'dependencies' and 'devDependencies'
You may have noticed that there are two entries containing dependency - dependencies
and devDependencies
.
dependencies
are always installed (or bundled) when either your web project is installed or in cases where you develop a library and your package is installed as a dependency of another project.
devDependencies
are only installed when developing the project (meaning that when you directly run install
in the specific directory) and they are otherwise not included in your project.
How do I install another package or dependency and how to use it?
The Installing section taught us that you can install dependencies by running npm i <package_name>
in your project directory where the package_name
can be any package that you find on npm.js.
Let's assume you want to add a tweening library to your project. We will use @tweenjs/tween.js
for this example. Here is the final project if you want to jump ahead and just see the result.
First run npm install @tweenjs/tween.js
in the terminal and wait for the installation to finish. This will add a new entry to our package.json:
"dependencies": {
"@needle-tools/engine": "^3.5.11-beta",
"@tweenjs/tween.js": "^20.0.3",
"three": "npm:@needle-tools/[email protected]"
}
Then open one of your script files in which you want to use tweening and import at the top of the file:
import * as TWEEN from '@tweenjs/tween.js';
Note that we do here import all types in the library by writing * as TWEEN
. We could also just import specific types like import { Tween } from @tweenjs/tween.js
.
Now we can use it in our script. It is always recommended to refer to the documentation of the library that you want to use. In the case of tween.js they provide a user guide that we can follow. Usually the Readme page of the package on npm contains information on how to install and use the package.
To rotate a cube we create a new component type called TweenRotation
, we then go ahead and create our tween instance for the object rotation, how often it should repeat, which easing to use, the tween we want to perform and then we start it. We then only have to call update
every frame to update the tween animation. The final script looks like this:
import { Behaviour } from "@needle-tools/engine";
import * as TWEEN from '@tweenjs/tween.js';
export class TweenRotation extends Behaviour {
// save the instance of our tweener
private _tween?: TWEEN.Tween<any>;
start() {
const rotation = this.gameObject.rotation;
// create the tween instance
this._tween = new TWEEN.Tween(rotation);
// set it to repeat forever
this._tween.repeat(Infinity);
// set the easing to use
this._tween.easing(TWEEN.Easing.Quintic.InOut);
// set the values to tween
this._tween.to({ y: Math.PI * 0.5 }, 1000);
// start it
this._tween.start();
}
update() {
// update the tweening every frame
// the '?' is a shorthand for checking if _tween has been created
this._tween?.update();
}
}
Now we only have to add it to any of the objects in our scene to rotate them forever.
You can see the final script in action here.