Files
pelpanagiotis a7c53a08a0 Initial commit: Unity Needle AR Menu project with MenuScene and SampleScene web apps
Add root .gitignore for Unity Library/Temp/Logs, IDE folders, and node_modules.
Include Assets, Needle TypeScript (MenuController, asset picker, WebXR), and project configuration.

Made-with: Cursor
2026-04-19 22:41:05 +03:00

277 lines
7.4 KiB
Markdown

# Needle Engine — Component Examples
Practical examples of common component patterns. All components extend `Behaviour` from `@needle-tools/engine`.
---
## Rotate an object
```ts
import { Behaviour, serializable, registerType } from "@needle-tools/engine";
@registerType
export class Rotate extends Behaviour {
@serializable() speed: number = 1;
update() {
this.gameObject.rotation.y += this.speed * this.context.time.deltaTime;
}
}
```
---
## Respond to clicks
```ts
import { Behaviour, registerType } from "@needle-tools/engine";
import type { PointerEventData } from "@needle-tools/engine";
@registerType
export class ClickHandler extends Behaviour {
onPointerClick(args: PointerEventData) {
console.log("Clicked:", this.gameObject.name);
// Scale up briefly on click
this.gameObject.scale.multiplyScalar(1.2);
setTimeout(() => this.gameObject.scale.multiplyScalar(1 / 1.2), 200);
}
// Other pointer events: onPointerEnter, onPointerExit, onPointerDown, onPointerUp, onPointerMove
}
```
Pointer events require an `EventSystem` and a `Raycaster` component in the scene (both are included by default in Unity/Blender exports).
---
## Load a GLB at runtime
```ts
import { Behaviour, AssetReference, serializable, registerType } from "@needle-tools/engine";
import { Object3D } from "three";
@registerType
export class RuntimeLoader extends Behaviour {
// Set in Unity/Blender inspector, or assign from code
@serializable(AssetReference) model?: AssetReference;
async start() {
// Option 1: From a serialized AssetReference
if (this.model) {
const instance = await this.model.instantiate({ parent: this.gameObject });
}
// Option 2: From a URL (code-only)
const ref = AssetReference.getOrCreate(this.context.domElement.baseURI, "assets/chair.glb");
const chair = await ref.instantiate({ parent: this.gameObject });
}
}
```
---
## Synced multiplayer state
```ts
import { Behaviour, serializable, registerType, syncField } from "@needle-tools/engine";
@registerType
export class SyncedCounter extends Behaviour {
// @syncField handles networking sync; add @serializable() too if the field should also
// deserialize from GLB (when set in Unity/Blender). For code-only components, @syncField alone is fine.
@syncField(SyncedCounter.prototype.onCountChanged)
count: number = 0;
private onCountChanged(newValue: number, oldValue: number) {
console.log(`Count changed: ${oldValue}${newValue}`);
}
increment() {
this.count += 1; // reassignment triggers sync
}
// For arrays/objects: must reassign to trigger sync
@syncField() tags: string[] = [];
addTag(tag: string) {
this.tags.push(tag);
this.tags = this.tags; // force sync
}
}
```
---
## Change materials at runtime
```ts
import { Behaviour, Renderer, registerType } from "@needle-tools/engine";
import { MeshStandardMaterial, Color } from "three";
@registerType
export class ColorChanger extends Behaviour {
onPointerClick() {
// Option 1: Via Renderer component (if available, e.g. from Unity/Blender export)
const renderer = this.gameObject.getComponent(Renderer);
if (renderer?.sharedMaterial) {
(renderer.sharedMaterial as MeshStandardMaterial).color = new Color(Math.random(), Math.random(), Math.random());
}
// Option 2: Direct Three.js access (always works)
this.gameObject.traverse(child => {
if ((child as any).material) {
(child as any).material.color = new Color(Math.random(), Math.random(), Math.random());
}
});
}
}
```
---
## Set up a scene from code (no Unity/Blender)
Use `onStart` to safely access the context — never poll with `setInterval`.
```ts
import { onStart, ObjectUtils, PrimitiveType, ContactShadows, SyncedRoom } from "@needle-tools/engine";
import { DirectionalLight, AmbientLight } from "three";
onStart(ctx => {
// Lighting
const dirLight = new DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
ctx.scene.add(dirLight);
ctx.scene.add(new AmbientLight(0xffffff, 0.5));
// Ground
const ground = ObjectUtils.createPrimitive(PrimitiveType.Cube, {
color: 0x888888,
scale: { x: 10, y: 0.1, z: 10 }
});
ctx.scene.add(ground);
// Some objects
for (let i = 0; i < 5; i++) {
const sphere = ObjectUtils.createPrimitive(PrimitiveType.Sphere, {
color: Math.random() * 0xffffff,
position: { x: (i - 2) * 2, y: 1, z: 0 }
});
ctx.scene.add(sphere);
}
// Shadows
ContactShadows.auto(ctx);
});
```
---
## Keyboard input
```ts
import { Behaviour, registerType } from "@needle-tools/engine";
import { Vector3 } from "three";
@registerType
export class KeyboardMover extends Behaviour {
speed: number = 5;
update() {
const dt = this.context.time.deltaTime;
const input = this.context.input;
// Key codes: use KeyCode values ("KeyW", "Space", "ArrowLeft") or lowercase letters ("w")
if (input.getKeyPressed("KeyW")) this.gameObject.position.z -= this.speed * dt;
if (input.getKeyPressed("KeyS")) this.gameObject.position.z += this.speed * dt;
if (input.getKeyPressed("KeyA")) this.gameObject.position.x -= this.speed * dt;
if (input.getKeyPressed("KeyD")) this.gameObject.position.x += this.speed * dt;
if (input.getKeyDown("Space")) {
console.log("Jump!");
}
}
}
```
---
## First-person camera movement
Three.js cameras look down `-Z`, so `getWorldDirection` returns the negated forward. Use `worldForward` from Needle's Object3D extensions instead — it handles this correctly. Note: `worldForward` works on `ctx.mainCamera` because Needle patches all Object3D instances, including Three.js cameras.
```ts
import { Behaviour, registerType } from "@needle-tools/engine";
import { Vector3 } from "three";
@registerType
export class FirstPersonMove extends Behaviour {
speed: number = 5;
private _forward = new Vector3();
private _right = new Vector3();
update() {
const dt = this.context.time.deltaTime;
const input = this.context.input;
const cam = this.context.mainCamera;
if (!cam) return;
// Use Needle's worldForward/worldRight — correctly handles Three.js -Z convention
this._forward.copy(cam.worldForward);
this._forward.y = 0;
this._forward.normalize();
this._right.copy(cam.worldRight);
this._right.y = 0;
this._right.normalize();
let moveX = 0, moveZ = 0;
if (input.getKeyPressed("KeyW")) { moveX += this._forward.x; moveZ += this._forward.z; }
if (input.getKeyPressed("KeyS")) { moveX -= this._forward.x; moveZ -= this._forward.z; }
if (input.getKeyPressed("KeyA")) { moveX -= this._right.x; moveZ -= this._right.z; }
if (input.getKeyPressed("KeyD")) { moveX += this._right.x; moveZ += this._right.z; }
const len = Math.sqrt(moveX * moveX + moveZ * moveZ);
if (len > 0) {
cam.position.x += (moveX / len) * this.speed * dt;
cam.position.z += (moveZ / len) * this.speed * dt;
}
}
}
```
---
## Coroutine (timed sequence)
```ts
import { Behaviour, registerType, WaitForSeconds } from "@needle-tools/engine";
@registerType
export class TrafficLight extends Behaviour {
start() {
this.startCoroutine(this.cycle());
}
*cycle() {
while (true) {
this.setColor("green");
yield WaitForSeconds(5);
this.setColor("yellow");
yield WaitForSeconds(2);
this.setColor("red");
yield WaitForSeconds(5);
}
}
private setColor(color: string) {
console.log("Light:", color);
}
}
```