Script Examples

Complete, production-ready script examples for common game mechanics. Copy and adapt these patterns for your project.

This page provides complete, tested script examples for common game mechanics. Each example demonstrates best practices and performance patterns used in Web Engine.

Player Controller

WASD movement, mouse look, jumping

Camera Follow

Third-person and orbit cameras

Projectile Spawning

Bullet spawning with cooldowns

Health System

Damage, health, and death

Pickup Collection

Trigger-based item collection

AI Patrol

NavMesh-based enemy patrol

Player Controller#

A complete first-person or third-person character controller with WASD movement, mouse look, sprinting, and jumping. Requires CharacterController and RigidBody components.

PlayerController.ts
typescript
export default (api) => {
// Configuration
const speed = 5.0;
const sprintMultiplier = 2.0;
const jumpForce = 10.0;
const sensitivity = 0.002;
// State
let yaw = 0;
let pitch = 0;
const maxPitch = Math.PI / 3; // 60 degrees
// Pooled vectors (reuse to avoid GC)
const moveVec = { x: 0, y: 0, z: 0 };
return async (dt, time) => {
const input = api.input;
// Mouse Look
if (input.look.x !== 0 || input.look.y !== 0) {
yaw -= input.look.x * sensitivity;
pitch -= input.look.y * sensitivity;
pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch));
// Update rotation (only yaw for capsule)
const rot = api.rotation;
rot.y = yaw;
api.rotation = rot;
}
// Movement (relative to camera)
const moveLen = Math.sqrt(input.move.x ** 2 + input.move.y ** 2);
if (moveLen > 0.01) {
const currentSpeed = input.sprint ? speed * sprintMultiplier : speed;
const sin = Math.sin(yaw);
const cos = Math.cos(yaw);
// Forward/backward (input.move.y)
const fwdX = -sin * input.move.y;
const fwdZ = -cos * input.move.y;
// Left/right (input.move.x)
const rightX = cos * input.move.x;
const rightZ = -sin * input.move.x;
moveVec.x = (fwdX + rightX) * currentSpeed;
moveVec.y = 0;
moveVec.z = (fwdZ + rightZ) * currentSpeed;
api.controller.move(moveVec);
}
// Jumping
if (input.jumpDown && api.controller.isGrounded) {
api.applyImpulse({ x: 0, y: jumpForce, z: 0 });
}
};
};

Performance Notes

Reusing the moveVec object avoids creating new Vector3 instances every frame, reducing garbage collection pressure.

Camera Follow#

A smooth third-person camera that follows a target entity. Requires a Target component pointing to the player.

FollowCamera.ts
typescript
export default (api) => {
// Camera offset from target (local space)
const offset = { x: 0, y: 2, z: -5 };
const smoothness = 8.0;
// Pooled vectors
const desiredPos = { x: 0, y: 0, z: 0 };
return (dt, time) => {
if (!api.target.exists) return;
const target = api.target;
// Calculate desired position in world space
// Rotate offset by target's rotation
const rot = target.rotation;
const sin = Math.sin(rot.y);
const cos = Math.cos(rot.y);
// Rotate offset vector
const offsetX = offset.x * cos - offset.z * sin;
const offsetZ = offset.x * sin + offset.z * cos;
desiredPos.x = target.position.x + offsetX;
desiredPos.y = target.position.y + offset.y;
desiredPos.z = target.position.z + offsetZ;
// Smooth lerp toward desired position
const t = Math.min(dt * smoothness, 1.0);
const pos = api.position;
pos.x += (desiredPos.x - pos.x) * t;
pos.y += (desiredPos.y - pos.y) * t;
pos.z += (desiredPos.z - pos.z) * t;
api.position = pos;
// Look at target
api.lookAt(target.position);
};
};

Projectile Spawning#

Spawn bullets on input with a fire rate cooldown. Demonstrates prefab spawning and timer management.

Gun.ts
typescript
export default (api) => {
const fireRate = 0.15; // 6.67 shots per second
const bulletSpeed = 50.0;
const muzzleOffset = { x: 0, y: 0.5, z: -0.5 };
let cooldown = 0;
// Pooled vectors
const spawnPos = { x: 0, y: 0, z: 0 };
const spawnRot = { x: 0, y: 0, z: 0 };
return async (dt, time) => {
cooldown -= dt;
if (api.input.fireDown && cooldown <= 0) {
cooldown = fireRate;
// Calculate spawn position (muzzle)
const pos = api.position;
const rot = api.rotation;
const sin = Math.sin(rot.y);
const cos = Math.cos(rot.y);
spawnPos.x = pos.x + muzzleOffset.x * cos - muzzleOffset.z * sin;
spawnPos.y = pos.y + muzzleOffset.y;
spawnPos.z = pos.z + muzzleOffset.x * sin + muzzleOffset.z * cos;
spawnRot.x = rot.x;
spawnRot.y = rot.y;
spawnRot.z = rot.z;
// Spawn bullet prefab
const bulletEid = await api.spawn("Bullet", spawnPos, spawnRot);
// Play sound effect
api.audio.play();
api.log("Fired bullet: " + bulletEid);
}
};
};

Prefab Setup

This script requires a Bullet prefab with a RigidBody component set to Dynamic type. The bullet should have its own script to handle velocity and lifetime.

Health/Damage System#

A damage-able entity with health, taking damage on collision. Demonstrates collision events and entity destruction.

Health.ts
typescript
export default (api) => {
let health = 100;
const maxHealth = 100;
const damagePerHit = 10;
// Invincibility frames to prevent multiple hits in one collision
let invincible = false;
let invincibleTimer = 0;
const invincibleDuration = 0.5; // 500ms
return {
onUpdate: (dt, time) => {
if (invincible) {
invincibleTimer -= dt;
if (invincibleTimer <= 0) {
invincible = false;
}
}
},
onCollisionEnter: (otherEid) => {
// Only take damage if not invincible
if (invincible) return;
// Check if collided with a damage source (e.g., bullet)
// You could add tags or check component types here
health -= damagePerHit;
invincible = true;
invincibleTimer = invincibleDuration;
api.log(`Took damage! Health: ${health}/${maxHealth}`);
// Visual feedback (optional)
// api.audio.play(); // Hit sound
// Check for death
if (health <= 0) {
api.log("Destroyed!");
// Spawn death effect
// await api.spawn("DeathEffect", api.position);
// The entity will be removed by the engine
}
},
onDestroy: (time) => {
api.log("Health component destroyed");
}
};
};

Pickup Collection#

A collectible item that triggers when the player enters its area. Requires a Sensor component for trigger detection.

Pickup.ts
typescript
export default (api) => {
const rotationSpeed = 2.0; // Rotate for visibility
const bobSpeed = 2.0;
const bobAmount = 0.2;
let collected = false;
const baseY = api.position.y;
return {
onUpdate: (dt, time) => {
if (collected) return;
// Rotate
const rot = api.rotation;
rot.y += rotationSpeed * dt;
api.rotation = rot;
// Bob up and down
const pos = api.position;
pos.y = baseY + Math.sin(time * bobSpeed) * bobAmount;
api.position = pos;
},
onTriggerEnter: (otherEid) => {
if (collected) return;
// Check if it's the player (you could add a tag system)
// For now, collect on any trigger
collected = true;
api.log(`Collected by entity ${otherEid}`);
// Play collect sound
api.audio.play();
// Grant item to player (implement via event system)
// events.emit("item_collected", { type: "coin", value: 10 });
// Destroy this entity
// The engine will handle cleanup after this frame
}
};
};

AI Patrol Behavior#

An AI agent that patrols between waypoints using the NavMesh system. Requires NavMeshAgent and Steering components.

AIPatrol.ts
typescript
export default (api) => {
// Waypoint positions
const waypoints = [
{ x: 0, y: 0, z: 0 },
{ x: 10, y: 0, z: 0 },
{ x: 10, y: 0, z: 10 },
{ x: 0, y: 0, z: 10 }
];
let currentWaypoint = 0;
let waitTimer = 0;
const waitDuration = 2.0; // Wait 2s at each waypoint
const reachedThreshold = 1.0; // Distance to consider "reached"
return {
onStart: (time) => {
// Start moving to first waypoint
api.navigation.setDestination(waypoints[currentWaypoint]);
api.navigation.setSpeed(3.0);
api.log("Starting patrol");
},
onUpdate: (dt, time) => {
// If waiting at waypoint
if (waitTimer > 0) {
waitTimer -= dt;
if (waitTimer <= 0) {
// Move to next waypoint
currentWaypoint = (currentWaypoint + 1) % waypoints.length;
api.navigation.setDestination(waypoints[currentWaypoint]);
api.log(`Moving to waypoint ${currentWaypoint}`);
}
return;
}
// Check if reached current waypoint
if (!api.navigation.isMoving && !api.navigation.isPathPending) {
const pos = api.position;
const target = waypoints[currentWaypoint];
const dx = target.x - pos.x;
const dz = target.z - pos.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < reachedThreshold) {
api.log(`Reached waypoint ${currentWaypoint}`);
waitTimer = waitDuration;
}
}
}
};
};

Dynamic Waypoints

For dynamic waypoints, you could store waypoint entities in the scene and reference them via Target components, or use an event system to update waypoints at runtime.

Bullet Script (Bonus)#

A companion script for the projectile spawning example. Handles bullet velocity and automatic destruction after a timeout.

Bullet.ts
typescript
export default (api) => {
const speed = 50.0;
const lifetime = 5.0; // Destroy after 5 seconds
let aliveTime = 0;
return {
onStart: (time) => {
// Apply initial velocity in forward direction
const rot = api.rotation;
const sin = Math.sin(rot.y);
const cos = Math.cos(rot.y);
api.velocity = {
x: -sin * speed,
y: 0,
z: -cos * speed
};
},
onUpdate: (dt, time) => {
aliveTime += dt;
// Destroy after lifetime
if (aliveTime >= lifetime) {
api.log("Bullet expired");
// Entity will be removed by engine
}
},
onCollisionEnter: (otherEid) => {
api.log("Bullet hit entity " + otherEid);
// Spawn impact effect
// await api.spawn("ImpactEffect", api.position);
// Destroy bullet
// Entity will be removed by engine
}
};
};
Script Examples - Web Engine Docs | Web Engine Docs