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.
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.
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.
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.
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.
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.
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.
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 } };};