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
This commit is contained in:
276
.agents/skills/needle-engine/references/examples.md
Normal file
276
.agents/skills/needle-engine/references/examples.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user