Scripting Introduction for Unity Developers
Last updated
Last updated
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 for a basic understanding between the differences between C# and Javascript/Typescript.
If you want to code-along you can to create a small project that you can edit in the browser ⚡
Needle Engine is a 3d web engine running on-top of . 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 and .
:::details How to create a new Unity project with Needle Engine? (Video) :::
In Unity you create a new component by deriving from MonoBehaviour
:
A custom component in Needle Engine on the other hand is written as follows:
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:
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.
Field without any accessor modified like private
, public
or protected
will by default be public
in javascript
The same is true for methods as well.
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:
or you can iterate using the foreach
equivalent:
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:
For more information about getting components see the next section.
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.
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)
.
Some Unity-specific types are mapped to different type names in our engine. See the following list:
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 data can be accessed on the GameObject
/ Object3D
directly. Unlike to Unity there is no extra transform component that holds this data.
this.gameObject.worldPosition
is the vector3 position in world space
this.gameObject.worldRotation
is the euler rotation in euler angles in world space
this.gameObject.worldQuaternion
is the quaternion rotation in world space
this.gameObject.worldScale
is the vector3 scale in world 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.
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"
Use this.context.time
to get access to time data:
this.context.time.time
is the time since the application started running
this.context.time.deltaTime
is the time that has passed since the last frame
this.context.time.frameCount
is the number of frames that have passed since the application started
this.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.
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.
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.
Use this.context.input
to poll input state:
You can also subscribe to events in the InputEvents
enum like so:
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).
To make this work make sure your object has a ObjectRaycaster
or GraphicRaycaster
component in the parent hierarchy.
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.
The Debug.Log()
equivalent in javascript is console.log()
. You can also use console.warn()
or console.error()
.
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
Here are some of the available gizmo methods:
Gizmos.DrawArrow
Gizmos.DrawBox
Gizmos.DrawBox3
Gizmos.DrawDirection
Gizmos.DrawLine
Gizmos.DrawRay
Gizmos.DrawRay
Gizmos.DrawSphere
Gizmos.DrawWireSphere
Import from @needle-tools/engine
e.g. import { getParam } from "@needle-tools/engine"
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
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
.
Here is an example of how a package.json might look like:
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.
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.
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:
Then open one of your script files in which you want to use tweening and import at the top of the file:
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
.
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:
Note that in some cases the type can be ommitted. This can be done for all . These are boolean
, number
, bigint
, string
, null
and undefined
.
You can also use three.js specific methods to quickly iterate all objects recursively using the method:
or to just traverse visible objects use instead.
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
this.gameObject.position
is the vector3 in local space
this.gameObject.rotation
is the in local space
this.gameObject.quaternion
- is the in local space
this.gameObject.scale
- is the vector3 in local space
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
Use this.context.physics.raycastFromRay(your_ray)
to perform a raycast using a
Here is a editable
If you want to handle inputs yourself you can also subscribe to (there are a ton). For example to subscribe to the browsers click event you can write:
Similar to Unity (see ) you can also register to receive input events on the component itself.
When working with a web project most of you dependencies are installed from . It is the most popular package registry out there for web projects.
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 . 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.
The 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 .
Let's assume you want to add a tweening library to your project. We will use for this example. is the final project if you want to jump ahead and just see the result.
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 that we can follow. Usually the Readme page of the package on npm contains information on how to install and use the package.
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 .