
Playcanvas Engine
- 1.4k installs
- 629 repo stars
- Updated November 20, 2025
- freshtechbro/claudedesignskills
playcanvas-engine provides documented workflows for Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interac
About
The playcanvas-engine skill lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interactive 3D applications, or performance-critical web experiences. Triggers on tasks involving PlayCanvas, entity-component systems, game engine development, WebGL games, 3D browser applications, editor-first workflows, or real-time 3D rendering. Alternative to Three.js with game-specific features and integrated development environment. # PlayCanvas Engine Skill Lightweight WebGL/WebGPU game engine with entity-component architecture, visual editor integration, and performance-focused design. ## When to Use This Skill Trigger this skill when you see: - "PlayCanvas engine" - "WebGL game engine" - "entity component system" - "PlayCanvas application" - "3D browser games" - "online 3D editor" - "lightweight 3D engine" - Need for editor-first workflow Compare with: - **Three.js**: Lower-level, more flexible but requires more setup - **Babylon.js**: Feature-rich but heavier, has editor but less mature - **A-Frame**: VR-focused, declarative HTML approach - Use PlayCanvas for: Game projects, editor-first work.
- "PlayCanvas engine"
- "WebGL game engine"
- "entity component system"
- "PlayCanvas application"
- "3D browser games"
Playcanvas Engine by the numbers
- 1,364 all-time installs (skills.sh)
- +91 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #173 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
playcanvas-engine capabilities & compatibility
- Capabilities
- "playcanvas engine" · "webgl game engine" · "entity component system" · "playcanvas application" · "3d browser games"
- Use cases
- documentation
What playcanvas-engine says it does
# PlayCanvas Engine Skill Lightweight WebGL/WebGPU game engine with entity-component architecture, visual editor integration, and performance-focused design.
Application The root PlayCanvas application manages the rendering loop.
npx skills add https://github.com/freshtechbro/claudedesignskills --skill playcanvas-engineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 629 |
| Security audit | 2 / 3 scanners passed |
| Last updated | November 20, 2025 |
| Repository | freshtechbro/claudedesignskills ↗ |
How do I use playcanvas-engine for the task described in its SKILL.md triggers?
Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interactive 3D applications, or performance-cr.
Who is it for?
Teams invoking playcanvas-engine when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interactive 3D applications, or performance-critical web experience
What you get
Step-by-step guidance grounded in playcanvas-engine documentation and reference files.
- PlayCanvas scene snippets
- Physics and animation examples
- UI and audio integration samples
By the numbers
- Documents 8 PlayCanvas example categories from basics through integration patterns
Files
PlayCanvas Engine Skill
Lightweight WebGL/WebGPU game engine with entity-component architecture, visual editor integration, and performance-focused design.
When to Use This Skill
Trigger this skill when you see:
- "PlayCanvas engine"
- "WebGL game engine"
- "entity component system"
- "PlayCanvas application"
- "3D browser games"
- "online 3D editor"
- "lightweight 3D engine"
- Need for editor-first workflow
Compare with:
- Three.js: Lower-level, more flexible but requires more setup
- Babylon.js: Feature-rich but heavier, has editor but less mature
- A-Frame: VR-focused, declarative HTML approach
- Use PlayCanvas for: Game projects, editor-first workflow, performance-critical apps
---
Core Concepts
1. Application
The root PlayCanvas application manages the rendering loop.
import * as pc from 'playcanvas';
// Create canvas
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
// Create application
const app = new pc.Application(canvas, {
keyboard: new pc.Keyboard(window),
mouse: new pc.Mouse(canvas),
touch: new pc.TouchDevice(canvas),
gamepads: new pc.GamePads()
});
// Configure canvas
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
// Handle resize
window.addEventListener('resize', () => app.resizeCanvas());
// Start the application
app.start();---
2. Entity-Component System
PlayCanvas uses ECS architecture: Entities contain Components.
// Create entity
const entity = new pc.Entity('myEntity');
// Add to scene hierarchy
app.root.addChild(entity);
// Add components
entity.addComponent('model', {
type: 'box'
});
entity.addComponent('script');
// Transform
entity.setPosition(0, 1, 0);
entity.setEulerAngles(0, 45, 0);
entity.setLocalScale(2, 2, 2);
// Parent-child hierarchy
const parent = new pc.Entity('parent');
const child = new pc.Entity('child');
parent.addChild(child);---
3. Update Loop
The application fires events during the update loop.
app.on('update', (dt) => {
// dt is delta time in seconds
entity.rotate(0, 10 * dt, 0);
});
app.on('prerender', () => {
// Before rendering
});
app.on('postrender', () => {
// After rendering
});---
4. Components
Core components extend entity functionality:
Model Component:
entity.addComponent('model', {
type: 'box', // 'box', 'sphere', 'cylinder', 'cone', 'capsule', 'asset'
material: material,
castShadows: true,
receiveShadows: true
});Camera Component:
entity.addComponent('camera', {
clearColor: new pc.Color(0.1, 0.2, 0.3),
fov: 45,
nearClip: 0.1,
farClip: 1000,
projection: pc.PROJECTION_PERSPECTIVE // or PROJECTION_ORTHOGRAPHIC
});Light Component:
entity.addComponent('light', {
type: pc.LIGHTTYPE_DIRECTIONAL, // DIRECTIONAL, POINT, SPOT
color: new pc.Color(1, 1, 1),
intensity: 1,
castShadows: true,
shadowDistance: 50
});Rigidbody Component (requires physics):
entity.addComponent('rigidbody', {
type: pc.BODYTYPE_DYNAMIC, // STATIC, DYNAMIC, KINEMATIC
mass: 1,
friction: 0.5,
restitution: 0.3
});
entity.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
});---
Common Patterns
Pattern 1: Basic Scene Setup
Create a complete scene with camera, light, and models.
import * as pc from 'playcanvas';
// Initialize application
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());
// Create camera
const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.2, 0.3, 0.4)
});
camera.setPosition(0, 2, 5);
camera.lookAt(0, 0, 0);
app.root.addChild(camera);
// Create directional light
const light = new pc.Entity('light');
light.addComponent('light', {
type: pc.LIGHTTYPE_DIRECTIONAL,
castShadows: true
});
light.setEulerAngles(45, 30, 0);
app.root.addChild(light);
// Create ground
const ground = new pc.Entity('ground');
ground.addComponent('model', {
type: 'plane'
});
ground.setLocalScale(10, 1, 10);
app.root.addChild(ground);
// Create cube
const cube = new pc.Entity('cube');
cube.addComponent('model', {
type: 'box',
castShadows: true
});
cube.setPosition(0, 1, 0);
app.root.addChild(cube);
// Animate cube
app.on('update', (dt) => {
cube.rotate(10 * dt, 20 * dt, 30 * dt);
});
app.start();---
Pattern 2: Loading GLTF Models
Load external 3D models with asset management.
// Create asset for model
const modelAsset = new pc.Asset('model', 'container', {
url: '/models/character.glb'
});
// Add to asset registry
app.assets.add(modelAsset);
// Load asset
modelAsset.ready((asset) => {
// Create entity from loaded model
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
// Scale and position
entity.setLocalScale(2, 2, 2);
entity.setPosition(0, 0, 0);
});
app.assets.load(modelAsset);With error handling:
modelAsset.ready((asset) => {
console.log('Model loaded:', asset.name);
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
});
modelAsset.on('error', (err) => {
console.error('Failed to load model:', err);
});
app.assets.load(modelAsset);---
Pattern 3: Materials and Textures
Create custom materials with PBR workflow.
// Create material
const material = new pc.StandardMaterial();
material.diffuse = new pc.Color(1, 0, 0); // Red
material.metalness = 0.5;
material.gloss = 0.8;
material.update();
// Apply to entity
entity.model.material = material;
// With textures
const textureAsset = new pc.Asset('diffuse', 'texture', {
url: '/textures/brick_diffuse.jpg'
});
app.assets.add(textureAsset);
app.assets.load(textureAsset);
textureAsset.ready((asset) => {
material.diffuseMap = asset.resource;
material.update();
});
// PBR material with all maps
const pbrMaterial = new pc.StandardMaterial();
// Load all textures
const textures = {
diffuse: '/textures/albedo.jpg',
normal: '/textures/normal.jpg',
metalness: '/textures/metalness.jpg',
gloss: '/textures/roughness.jpg',
ao: '/textures/ao.jpg'
};
Object.keys(textures).forEach(key => {
const asset = new pc.Asset(key, 'texture', { url: textures[key] });
app.assets.add(asset);
asset.ready((loadedAsset) => {
switch(key) {
case 'diffuse':
pbrMaterial.diffuseMap = loadedAsset.resource;
break;
case 'normal':
pbrMaterial.normalMap = loadedAsset.resource;
break;
case 'metalness':
pbrMaterial.metalnessMap = loadedAsset.resource;
break;
case 'gloss':
pbrMaterial.glossMap = loadedAsset.resource;
break;
case 'ao':
pbrMaterial.aoMap = loadedAsset.resource;
break;
}
pbrMaterial.update();
});
app.assets.load(asset);
});---
Pattern 4: Physics Integration
Use Ammo.js for physics simulation.
import * as pc from 'playcanvas';
// Initialize with Ammo.js
const app = new pc.Application(canvas, {
keyboard: new pc.Keyboard(window),
mouse: new pc.Mouse(canvas)
});
// Load Ammo.js
const ammoScript = document.createElement('script');
ammoScript.src = 'https://cdn.jsdelivr.net/npm/ammo.js@0.0.10/ammo.js';
document.body.appendChild(ammoScript);
ammoScript.onload = () => {
Ammo().then((AmmoLib) => {
window.Ammo = AmmoLib;
// Create static ground
const ground = new pc.Entity('ground');
ground.addComponent('model', { type: 'plane' });
ground.setLocalScale(10, 1, 10);
ground.addComponent('rigidbody', {
type: pc.BODYTYPE_STATIC
});
ground.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(5, 0.1, 5)
});
app.root.addChild(ground);
// Create dynamic cube
const cube = new pc.Entity('cube');
cube.addComponent('model', { type: 'box' });
cube.setPosition(0, 5, 0);
cube.addComponent('rigidbody', {
type: pc.BODYTYPE_DYNAMIC,
mass: 1,
friction: 0.5,
restitution: 0.5
});
cube.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
});
app.root.addChild(cube);
// Apply force
cube.rigidbody.applyForce(10, 0, 0);
cube.rigidbody.applyTorque(0, 10, 0);
app.start();
});
};---
Pattern 5: Custom Scripts
Create reusable script components.
// Define script class
const RotateScript = pc.createScript('rotate');
// Script attributes (editor-exposed)
RotateScript.attributes.add('speed', {
type: 'number',
default: 10,
title: 'Rotation Speed'
});
RotateScript.attributes.add('axis', {
type: 'vec3',
default: [0, 1, 0],
title: 'Rotation Axis'
});
// Initialize method
RotateScript.prototype.initialize = function() {
console.log('RotateScript initialized');
};
// Update method (called every frame)
RotateScript.prototype.update = function(dt) {
this.entity.rotate(
this.axis.x * this.speed * dt,
this.axis.y * this.speed * dt,
this.axis.z * this.speed * dt
);
};
// Cleanup
RotateScript.prototype.destroy = function() {
console.log('RotateScript destroyed');
};
// Usage
const entity = new pc.Entity('rotatingCube');
entity.addComponent('model', { type: 'box' });
entity.addComponent('script');
entity.script.create('rotate', {
attributes: {
speed: 20,
axis: new pc.Vec3(0, 1, 0)
}
});
app.root.addChild(entity);Script lifecycle methods:
const MyScript = pc.createScript('myScript');
MyScript.prototype.initialize = function() {
// Called once after all resources are loaded
};
MyScript.prototype.postInitialize = function() {
// Called after all entities have initialized
};
MyScript.prototype.update = function(dt) {
// Called every frame before rendering
};
MyScript.prototype.postUpdate = function(dt) {
// Called every frame after update
};
MyScript.prototype.swap = function(old) {
// Hot reload support
};
MyScript.prototype.destroy = function() {
// Cleanup when entity is destroyed
};---
Pattern 6: Input Handling
Handle keyboard, mouse, and touch input.
// Keyboard
if (app.keyboard.isPressed(pc.KEY_W)) {
entity.translate(0, 0, -speed * dt);
}
if (app.keyboard.wasPressed(pc.KEY_SPACE)) {
entity.rigidbody.applyImpulse(0, 10, 0);
}
// Mouse
app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
if (event.button === pc.MOUSEBUTTON_LEFT) {
console.log('Left click at', event.x, event.y);
}
});
app.mouse.on(pc.EVENT_MOUSEMOVE, (event) => {
const dx = event.dx;
const dy = event.dy;
camera.rotate(-dy * 0.2, -dx * 0.2, 0);
});
// Touch
app.touch.on(pc.EVENT_TOUCHSTART, (event) => {
event.touches.forEach((touch) => {
console.log('Touch at', touch.x, touch.y);
});
});
// Raycasting (mouse picking)
app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
const camera = app.root.findByName('camera');
const cameraComponent = camera.camera;
const from = cameraComponent.screenToWorld(
event.x,
event.y,
cameraComponent.nearClip
);
const to = cameraComponent.screenToWorld(
event.x,
event.y,
cameraComponent.farClip
);
const result = app.systems.rigidbody.raycastFirst(from, to);
if (result) {
console.log('Hit:', result.entity.name);
result.entity.model.material.emissive = new pc.Color(1, 0, 0);
}
});---
Pattern 7: Animations
Play skeletal animations and tweens.
Skeletal animation:
// Load animated model
const modelAsset = new pc.Asset('character', 'container', {
url: '/models/character.glb'
});
app.assets.add(modelAsset);
modelAsset.ready((asset) => {
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
// Get animation component
entity.addComponent('animation', {
assets: [asset],
speed: 1.0,
loop: true,
activate: true
});
// Play specific animation
entity.animation.play('Walk', 0.2); // 0.2s blend time
// Later, transition to run
entity.animation.play('Run', 0.5);
});
app.assets.load(modelAsset);Property tweening:
// Animate position
entity.tween(entity.getLocalPosition())
.to({ x: 5, y: 2, z: 0 }, 2.0, pc.SineInOut)
.start();
// Animate rotation
entity.tween(entity.getLocalEulerAngles())
.to({ x: 0, y: 180, z: 0 }, 1.0, pc.Linear)
.loop(true)
.yoyo(true)
.start();
// Animate material color
const color = material.emissive;
app.tween(color)
.to(new pc.Color(1, 0, 0), 1.0, pc.SineInOut)
.yoyo(true)
.loop(true)
.start();
// Chain tweens
entity.tween(entity.getLocalPosition())
.to({ y: 2 }, 1.0)
.to({ y: 0 }, 1.0)
.delay(0.5)
.repeat(3)
.start();---
Integration Patterns
Integration 1: React Integration
Wrap PlayCanvas in React components.
import React, { useEffect, useRef } from 'react';
import * as pc from 'playcanvas';
function PlayCanvasScene() {
const canvasRef = useRef(null);
const appRef = useRef(null);
useEffect(() => {
// Initialize
const app = new pc.Application(canvasRef.current);
appRef.current = app;
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
// Create scene
const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.1, 0.2, 0.3)
});
camera.setPosition(0, 0, 5);
app.root.addChild(camera);
const cube = new pc.Entity('cube');
cube.addComponent('model', { type: 'box' });
app.root.addChild(cube);
const light = new pc.Entity('light');
light.addComponent('light');
light.setEulerAngles(45, 0, 0);
app.root.addChild(light);
app.on('update', (dt) => {
cube.rotate(10 * dt, 20 * dt, 30 * dt);
});
app.start();
// Cleanup
return () => {
app.destroy();
};
}, []);
return (
<canvas
ref={canvasRef}
style={{ width: '100%', height: '100vh' }}
/>
);
}
export default PlayCanvasScene;---
Integration 2: Editor Export
Work with PlayCanvas Editor projects.
// Export from PlayCanvas Editor
// Download build files, then load in code:
import * as pc from 'playcanvas';
const app = new pc.Application(canvas);
// Load exported project config
fetch('/config.json')
.then(response => response.json())
.then(config => {
// Load scene
app.scenes.loadSceneHierarchy(config.scene_url, (err, parent) => {
if (err) {
console.error('Failed to load scene:', err);
return;
}
// Start application
app.start();
// Find entities by name
const player = app.root.findByName('Player');
const enemy = app.root.findByName('Enemy');
// Access scripts
player.script.myScript.doSomething();
});
});---
Performance Optimization
1. Object Pooling
Reuse entities instead of creating/destroying.
class EntityPool {
constructor(app, count) {
this.app = app;
this.pool = [];
this.active = [];
for (let i = 0; i < count; i++) {
const entity = new pc.Entity('pooled');
entity.addComponent('model', { type: 'box' });
entity.enabled = false;
app.root.addChild(entity);
this.pool.push(entity);
}
}
spawn(position) {
let entity = this.pool.pop();
if (!entity) {
// Pool exhausted, create new
entity = new pc.Entity('pooled');
entity.addComponent('model', { type: 'box' });
this.app.root.addChild(entity);
}
entity.enabled = true;
entity.setPosition(position);
this.active.push(entity);
return entity;
}
despawn(entity) {
entity.enabled = false;
const index = this.active.indexOf(entity);
if (index > -1) {
this.active.splice(index, 1);
this.pool.push(entity);
}
}
}
// Usage
const pool = new EntityPool(app, 100);
const bullet = pool.spawn(new pc.Vec3(0, 0, 0));
// Later
pool.despawn(bullet);---
2. LOD (Level of Detail)
Reduce geometry for distant objects.
// Manual LOD switching
app.on('update', () => {
const distance = camera.getPosition().distance(entity.getPosition());
if (distance < 10) {
entity.model.asset = highResModel;
} else if (distance < 50) {
entity.model.asset = mediumResModel;
} else {
entity.model.asset = lowResModel;
}
});
// Or disable distant entities
app.on('update', () => {
entities.forEach(entity => {
const distance = camera.getPosition().distance(entity.getPosition());
entity.enabled = distance < 100;
});
});---
3. Batching
Combine static meshes to reduce draw calls.
// Enable static batching for entity
entity.model.batchGroupId = 1;
// Batch all entities with same group ID
app.batcher.generate([entity1, entity2, entity3]);---
4. Texture Compression
Use compressed texture formats.
// When creating textures, use compressed formats
const texture = new pc.Texture(app.graphicsDevice, {
width: 512,
height: 512,
format: pc.PIXELFORMAT_DXT5, // GPU-compressed
minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR,
magFilter: pc.FILTER_LINEAR,
mipmaps: true
});---
Common Pitfalls
Pitfall 1: Not Starting the Application
Problem: Scene renders but nothing happens.
// ❌ Wrong - forgot to start
const app = new pc.Application(canvas);
// ... create entities ...
// Nothing happens!
// ✅ Correct
const app = new pc.Application(canvas);
// ... create entities ...
app.start(); // Critical!---
Pitfall 2: Modifying Entities During Update
Problem: Modifying scene graph during iteration.
// ❌ Wrong - modifying array during iteration
app.on('update', () => {
entities.forEach(entity => {
if (entity.shouldDestroy) {
entity.destroy(); // Modifies array!
}
});
});
// ✅ Correct - mark for deletion, clean up after
const toDestroy = [];
app.on('update', () => {
entities.forEach(entity => {
if (entity.shouldDestroy) {
toDestroy.push(entity);
}
});
});
app.on('postUpdate', () => {
toDestroy.forEach(entity => entity.destroy());
toDestroy.length = 0;
});---
Pitfall 3: Memory Leaks with Assets
Problem: Not cleaning up loaded assets.
// ❌ Wrong - assets never cleaned up
function loadModel() {
const asset = new pc.Asset('model', 'container', { url: '/model.glb' });
app.assets.add(asset);
app.assets.load(asset);
// Asset stays in memory forever
}
// ✅ Correct - clean up when done
function loadModel() {
const asset = new pc.Asset('model', 'container', { url: '/model.glb' });
app.assets.add(asset);
asset.ready(() => {
// Use model
});
app.assets.load(asset);
// Clean up later
return () => {
app.assets.remove(asset);
asset.unload();
};
}
const cleanup = loadModel();
// Later: cleanup();---
Pitfall 4: Incorrect Transform Hierarchy
Problem: Transforms not propagating correctly.
// ❌ Wrong - setting world transform on child
const parent = new pc.Entity();
const child = new pc.Entity();
parent.addChild(child);
child.setPosition(5, 0, 0); // Local position
parent.setPosition(10, 0, 0);
// Child is at (15, 0, 0) in world space
// ✅ Correct - understand local vs world
child.setLocalPosition(5, 0, 0); // Explicit local
// or
const worldPos = new pc.Vec3(15, 0, 0);
child.setPosition(worldPos); // Explicit world---
Pitfall 5: Physics Not Initialized
Problem: Physics components don't work.
// ❌ Wrong - Ammo.js not loaded
const entity = new pc.Entity();
entity.addComponent('rigidbody', { type: pc.BODYTYPE_DYNAMIC });
// Error: Ammo is not defined
// ✅ Correct - ensure Ammo.js is loaded
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/ammo.js@0.0.10/ammo.js';
document.body.appendChild(script);
script.onload = () => {
Ammo().then((AmmoLib) => {
window.Ammo = AmmoLib;
// Now physics works
const entity = new pc.Entity();
entity.addComponent('rigidbody', { type: pc.BODYTYPE_DYNAMIC });
entity.addComponent('collision', { type: 'box' });
});
};---
Pitfall 6: Canvas Sizing Issues
Problem: Canvas doesn't fill container or respond to resize.
// ❌ Wrong - fixed size canvas
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
// ✅ Correct - responsive canvas
const canvas = document.createElement('canvas');
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());---
Resources
- Official API: https://api.playcanvas.com/
- Developer Docs: https://developer.playcanvas.com/
- Examples: https://playcanvas.github.io/
- Editor: https://playcanvas.com/
- GitHub: https://github.com/playcanvas/engine
- Forum: https://forum.playcanvas.com/
---
Quick Reference
Application Setup
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
app.start();Entity Creation
const entity = new pc.Entity('name');
entity.addComponent('model', { type: 'box' });
entity.setPosition(x, y, z);
app.root.addChild(entity);Update Loop
app.on('update', (dt) => {
// Logic here
});Loading Assets
const asset = new pc.Asset('name', 'type', { url: '/path' });
app.assets.add(asset);
asset.ready(() => { /* use asset */ });
app.assets.load(asset);---
Related Skills: For lower-level WebGL control, reference threejs-webgl. For React integration patterns, see react-three-fiber. For physics-heavy simulations, reference babylonjs-engine.
PlayCanvas Examples
Comprehensive collection of PlayCanvas examples, patterns, and use cases.
---
Table of Contents
1. Basic Examples 2. 3D Graphics 3. Physics & Interaction 4. Animation 5. User Interface 6. Audio 7. Performance 8. Integration Patterns
---
Basic Examples
1. Hello Cube
Minimal PlayCanvas scene with rotating cube:
const canvas = document.getElementById('canvas');
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
// Camera
const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.2, 0.3, 0.4)
});
camera.setPosition(0, 0, 5);
app.root.addChild(camera);
// Light
const light = new pc.Entity('light');
light.addComponent('light');
light.setEulerAngles(45, 45, 0);
app.root.addChild(light);
// Cube
const cube = new pc.Entity('cube');
cube.addComponent('model', { type: 'box' });
app.root.addChild(cube);
// Rotate cube
app.on('update', (dt) => {
cube.rotate(10 * dt, 20 * dt, 30 * dt);
});
app.start();---
2. Multiple Objects
Creating multiple objects in a scene:
const shapes = ['box', 'sphere', 'cylinder', 'cone', 'capsule'];
const colors = [
new pc.Color(1, 0, 0), // Red
new pc.Color(0, 1, 0), // Green
new pc.Color(0, 0, 1), // Blue
new pc.Color(1, 1, 0), // Yellow
new pc.Color(1, 0, 1) // Magenta
];
shapes.forEach((shape, i) => {
const entity = new pc.Entity(shape);
entity.addComponent('model', { type: shape });
// Position in grid
entity.setPosition((i - 2) * 2, 0, 0);
// Create material
const material = new pc.StandardMaterial();
material.diffuse = colors[i];
material.update();
entity.model.material = material;
app.root.addChild(entity);
});---
3. Loading 3D Models
Load GLTF/GLB models:
// Create asset for model
const asset = new pc.Asset('model', 'container', {
url: 'path/to/model.glb'
});
app.assets.add(asset);
// Load asset
app.assets.load(asset);
asset.ready((asset) => {
const entity = asset.resource.instantiateRenderEntity();
entity.setPosition(0, 0, 0);
app.root.addChild(entity);
});
asset.on('error', (err) => {
console.error('Failed to load model:', err);
});---
3D Graphics
1. Materials & Textures
PBR material with textures:
// Create material
const material = new pc.StandardMaterial();
// Diffuse (albedo) texture
const diffuseAsset = new pc.Asset('diffuse', 'texture', {
url: 'textures/albedo.jpg'
});
app.assets.add(diffuseAsset);
app.assets.load(diffuseAsset);
diffuseAsset.ready(() => {
material.diffuseMap = diffuseAsset.resource;
material.update();
});
// Normal map
const normalAsset = new pc.Asset('normal', 'texture', {
url: 'textures/normal.jpg'
});
app.assets.add(normalAsset);
app.assets.load(normalAsset);
normalAsset.ready(() => {
material.normalMap = normalAsset.resource;
material.update();
});
// Metalness
material.metalness = 0.7;
material.gloss = 0.8;
// Apply to entity
entity.model.material = material;---
2. Dynamic Lighting
Multiple light types:
// Directional Light (Sun)
const sun = new pc.Entity('sun');
sun.addComponent('light', {
type: 'directional',
color: new pc.Color(1, 1, 0.9),
intensity: 1.5,
castShadows: true,
shadowBias: 0.2,
shadowDistance: 40
});
sun.setEulerAngles(45, 30, 0);
app.root.addChild(sun);
// Point Light
const pointLight = new pc.Entity('pointLight');
pointLight.addComponent('light', {
type: 'point',
color: new pc.Color(1, 0, 0),
intensity: 1,
range: 10,
castShadows: false
});
pointLight.setPosition(0, 2, 0);
app.root.addChild(pointLight);
// Spot Light
const spotLight = new pc.Entity('spotLight');
spotLight.addComponent('light', {
type: 'spot',
color: new pc.Color(0, 1, 0),
intensity: 1,
range: 15,
innerConeAngle: 20,
outerConeAngle: 30,
castShadows: true
});
spotLight.setPosition(5, 5, 0);
spotLight.lookAt(0, 0, 0);
app.root.addChild(spotLight);---
3. Camera Effects
Post-processing and camera effects:
// Depth of Field
camera.camera.enablePostEffects = true;
// Create depth of field effect (requires custom script)
const dofScript = camera.script.create('depthOfField', {
attributes: {
focusDistance: 10,
aperture: 0.5,
maxBlur: 1.0
}
});
// Camera shake effect
function cameraShake(intensity, duration) {
const startTime = Date.now();
const originalPos = camera.getPosition().clone();
const shakeInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
if (elapsed >= duration) {
camera.setPosition(originalPos);
clearInterval(shakeInterval);
return;
}
const shakeAmount = intensity * (1 - elapsed / duration);
const x = originalPos.x + (Math.random() - 0.5) * shakeAmount;
const y = originalPos.y + (Math.random() - 0.5) * shakeAmount;
const z = originalPos.z + (Math.random() - 0.5) * shakeAmount;
camera.setPosition(x, y, z);
}, 16);
}
// Usage
cameraShake(0.5, 0.5); // intensity, duration---
Physics & Interaction
1. Basic Physics
Rigidbody and collision setup:
// Dynamic rigidbody (falls with gravity)
const dynamicBox = new pc.Entity('dynamicBox');
dynamicBox.addComponent('model', { type: 'box' });
dynamicBox.addComponent('rigidbody', {
type: 'dynamic',
mass: 1,
friction: 0.5,
restitution: 0.5 // Bounciness
});
dynamicBox.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
});
dynamicBox.setPosition(0, 5, 0);
app.root.addChild(dynamicBox);
// Static ground
const ground = new pc.Entity('ground');
ground.addComponent('model', { type: 'plane' });
ground.addComponent('rigidbody', {
type: 'static'
});
ground.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(10, 0.1, 10)
});
ground.setLocalScale(20, 1, 20);
app.root.addChild(ground);---
2. Raycasting & Object Picking
Pick objects with mouse:
function pickObject(screenX, screenY) {
const camera = app.root.findByName('Camera');
// Convert screen to world coordinates
const worldPos = camera.camera.screenToWorld(
screenX,
screenY,
camera.camera.farClip
);
const from = camera.getPosition();
const to = worldPos;
// Raycast
const result = app.systems.rigidbody.raycastFirst(from, to);
if (result) {
console.log('Hit:', result.entity.name);
return result.entity;
}
return null;
}
// Mouse click handler
app.mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
const picked = pickObject(event.x, event.y);
if (picked) {
// Do something with picked entity
picked.rigidbody.applyImpulse(0, 5, 0);
}
});---
3. Character Controller
WASD character movement:
var CharacterController = pc.createScript('characterController');
CharacterController.attributes.add('speed', {
type: 'number',
default: 5.0
});
CharacterController.prototype.initialize = function() {
this.moveDirection = new pc.Vec3();
};
CharacterController.prototype.update = function(dt) {
const keyboard = this.app.keyboard;
// Get camera direction
const camera = this.app.root.findByName('Camera');
const forward = camera.forward.clone();
const right = camera.right.clone();
// Flatten to horizontal
forward.y = 0;
forward.normalize();
right.y = 0;
right.normalize();
// Calculate movement
this.moveDirection.set(0, 0, 0);
if (keyboard.isPressed(pc.KEY_W)) {
this.moveDirection.add(forward);
}
if (keyboard.isPressed(pc.KEY_S)) {
this.moveDirection.sub(forward);
}
if (keyboard.isPressed(pc.KEY_A)) {
this.moveDirection.sub(right);
}
if (keyboard.isPressed(pc.KEY_D)) {
this.moveDirection.add(right);
}
// Apply movement
if (this.moveDirection.length() > 0) {
this.moveDirection.normalize();
this.moveDirection.scale(this.speed * dt);
const pos = this.entity.getPosition();
pos.add(this.moveDirection);
this.entity.setPosition(pos);
// Rotate to face movement direction
const angle = Math.atan2(this.moveDirection.x, this.moveDirection.z) * pc.math.RAD_TO_DEG;
this.entity.setEulerAngles(0, angle, 0);
}
};---
Animation
1. Property Animation (Tween)
Animate entity properties:
// Tween helper
function tween(entity, property, from, to, duration, easing = 'linear') {
const startTime = Date.now();
const easingFuncs = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
};
const easingFunc = easingFuncs[easing] || easingFuncs.linear;
const animate = () => {
const elapsed = (Date.now() - startTime) / 1000;
const t = Math.min(elapsed / duration, 1);
const eased = easingFunc(t);
const value = from + (to - from) * eased;
if (property === 'x' || property === 'y' || property === 'z') {
const pos = entity.getPosition();
pos[property] = value;
entity.setPosition(pos);
}
if (t < 1) {
requestAnimationFrame(animate);
}
};
animate();
}
// Usage
tween(entity, 'y', 0, 5, 2, 'easeInOut'); // Move up over 2 seconds---
2. Skeletal Animation
Animate 3D character models:
// Load model with animations
const modelAsset = new pc.Asset('character', 'container', {
url: 'models/character.glb'
});
app.assets.add(modelAsset);
app.assets.load(modelAsset);
modelAsset.ready((asset) => {
const entity = asset.resource.instantiateRenderEntity();
// Add animation component
entity.addComponent('anim', {
activate: true
});
// Get animation clips
const animations = asset.resource.animations;
// Create animation state graph
const animStateGraph = {
layers: [
{
name: 'locomotion',
states: [
{ name: 'idle', speed: 1.0 },
{ name: 'walk', speed: 1.0 },
{ name: 'run', speed: 1.0 }
],
transitions: [
{ from: 'idle', to: 'walk', duration: 0.2 },
{ from: 'walk', to: 'run', duration: 0.2 },
{ from: 'run', to: 'idle', duration: 0.3 }
]
}
]
};
// Load animation clips
animations.forEach(anim => {
entity.anim.assignAnimation(anim.name, anim.resource);
});
// Play animation
entity.anim.setBoolean('walk', true);
app.root.addChild(entity);
});---
3. Particle Systems
Create particle effects:
// Particle system entity
const particles = new pc.Entity('particles');
// Add particle system component
particles.addComponent('particlesystem', {
numParticles: 100,
lifetime: 2,
rate: 0.05,
emitterShape: pc.EMITTERSHAPE_SPHERE,
emitterRadius: 0.5,
// Velocity
velocityGraph: new pc.CurveSet([
[0, 0.1],
[1, 0.5]
]),
// Size
scaleGraph: new pc.CurveSet([
[0, 0.1],
[0.5, 0.5],
[1, 0.1]
]),
// Color
colorGraph: new pc.CurveSet([
[0, 1, 1, 0, 0], // Red at start
[0.5, 1, 0.5, 0, 0], // Orange
[1, 0.2, 0, 0, 0] // Dark at end
]),
// Alpha
alphaGraph: new pc.CurveSet([
[0, 0],
[0.2, 1],
[0.8, 1],
[1, 0]
]),
blendType: pc.BLEND_ADDITIVE,
depthWrite: false,
lighting: false
});
particles.setPosition(0, 2, 0);
app.root.addChild(particles);---
User Interface
1. 2D UI Elements
Create screen-space UI:
// Create screen entity (UI root)
const screen = new pc.Entity('screen');
screen.addComponent('screen', {
referenceResolution: new pc.Vec2(1280, 720),
scaleBlend: 0.5,
scaleMode: pc.SCALEMODE_BLEND,
screenSpace: true
});
app.root.addChild(screen);
// Create button
const button = new pc.Entity('button');
button.addComponent('element', {
anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5),
pivot: new pc.Vec2(0.5, 0.5),
width: 200,
height: 50,
type: pc.ELEMENTTYPE_IMAGE,
color: new pc.Color(0.2, 0.6, 1),
useInput: true
});
// Button text
const buttonText = new pc.Entity('buttonText');
buttonText.addComponent('element', {
anchor: new pc.Vec4(0.5, 0.5, 0.5, 0.5),
pivot: new pc.Vec2(0.5, 0.5),
fontSize: 24,
text: 'Click Me',
type: pc.ELEMENTTYPE_TEXT,
color: new pc.Color(1, 1, 1)
});
button.addChild(buttonText);
screen.addChild(button);
// Button click handler
button.element.on('click', () => {
console.log('Button clicked!');
});---
2. Health Bar
Dynamic UI health bar:
// Health bar background
const healthBg = new pc.Entity('healthBg');
healthBg.addComponent('element', {
anchor: new pc.Vec4(0, 1, 0, 1),
pivot: new pc.Vec2(0, 1),
margin: new pc.Vec4(20, 20, 0, 0),
width: 200,
height: 20,
type: pc.ELEMENTTYPE_IMAGE,
color: new pc.Color(0.2, 0.2, 0.2)
});
// Health bar fill
const healthFill = new pc.Entity('healthFill');
healthFill.addComponent('element', {
anchor: new pc.Vec4(0, 0, 0, 1),
pivot: new pc.Vec2(0, 0.5),
width: 200,
height: 16,
type: pc.ELEMENTTYPE_IMAGE,
color: new pc.Color(0, 1, 0)
});
healthBg.addChild(healthFill);
screen.addChild(healthBg);
// Update health
function setHealth(percent) {
healthFill.element.width = 200 * (percent / 100);
// Color based on health
if (percent > 50) {
healthFill.element.color = new pc.Color(0, 1, 0); // Green
} else if (percent > 25) {
healthFill.element.color = new pc.Color(1, 1, 0); // Yellow
} else {
healthFill.element.color = new pc.Color(1, 0, 0); // Red
}
}
// Usage
setHealth(75);---
Audio
1. Background Music
Load and play audio:
// Create audio asset
const musicAsset = new pc.Asset('music', 'audio', {
url: 'audio/music.mp3'
});
app.assets.add(musicAsset);
app.assets.load(musicAsset);
// Create audio entity
const music = new pc.Entity('music');
music.addComponent('sound');
musicAsset.ready(() => {
music.sound.addSlot('music', {
asset: musicAsset,
autoPlay: true,
loop: true,
volume: 0.5
});
});
app.root.addChild(music);
// Control playback
music.sound.play('music');
music.sound.pause('music');
music.sound.stop('music');
music.sound.slot('music').volume = 0.7;---
2. 3D Positional Audio
Spatial audio effects:
// 3D sound effect
const soundEntity = new pc.Entity('sound');
soundEntity.addComponent('sound', {
positional: true,
distanceModel: pc.DISTANCE_INVERSE,
refDistance: 1,
maxDistance: 20,
rollOffFactor: 1
});
soundEntity.setPosition(5, 0, 0);
const soundAsset = new pc.Asset('effect', 'audio', {
url: 'audio/explosion.mp3'
});
app.assets.add(soundAsset);
app.assets.load(soundAsset);
soundAsset.ready(() => {
soundEntity.sound.addSlot('effect', {
asset: soundAsset,
autoPlay: false,
loop: false,
volume: 1.0
});
});
app.root.addChild(soundEntity);
// Play sound at position
soundEntity.sound.play('effect');---
Performance
1. Object Pooling
Reuse objects instead of creating/destroying:
class ObjectPool {
constructor(app, template, initialSize = 10) {
this.app = app;
this.template = template;
this.available = [];
this.active = [];
// Pre-create objects
for (let i = 0; i < initialSize; i++) {
this.createObject();
}
}
createObject() {
const obj = this.template.clone();
obj.enabled = false;
this.app.root.addChild(obj);
this.available.push(obj);
return obj;
}
spawn(position) {
let obj = this.available.pop();
if (!obj) {
obj = this.createObject();
}
obj.enabled = true;
obj.setPosition(position);
this.active.push(obj);
return obj;
}
despawn(obj) {
obj.enabled = false;
const index = this.active.indexOf(obj);
if (index > -1) {
this.active.splice(index, 1);
this.available.push(obj);
}
}
reset() {
this.active.forEach(obj => {
obj.enabled = false;
this.available.push(obj);
});
this.active = [];
}
}
// Usage
const bulletTemplate = new pc.Entity('bullet');
bulletTemplate.addComponent('model', { type: 'sphere' });
bulletTemplate.setLocalScale(0.2, 0.2, 0.2);
const bulletPool = new ObjectPool(app, bulletTemplate, 50);
// Spawn bullet
const bullet = bulletPool.spawn(new pc.Vec3(0, 1, 0));
// Despawn after 2 seconds
setTimeout(() => {
bulletPool.despawn(bullet);
}, 2000);---
2. LOD (Level of Detail)
Optimize rendering with LOD:
// Create entity with multiple LOD levels
const entity = new pc.Entity('lodEntity');
entity.addComponent('model', { type: 'asset', asset: highPolyModel });
// Setup LOD levels
const meshInstances = entity.model.meshInstances;
meshInstances.forEach(meshInstance => {
// Define LOD levels
meshInstance.lodDistances = [10, 50, 100];
// High detail (< 10 units)
meshInstance.lod0 = highPolyMesh;
// Medium detail (10-50 units)
meshInstance.lod1 = mediumPolyMesh;
// Low detail (50-100 units)
meshInstance.lod2 = lowPolyMesh;
// Very low detail (> 100 units)
meshInstance.lod3 = veryLowPolyMesh;
});---
Integration Patterns
1. React Integration
Use PlayCanvas in React:
import React, { useEffect, useRef } from 'react';
import * as pc from 'playcanvas';
function PlayCanvasComponent() {
const canvasRef = useRef(null);
const appRef = useRef(null);
useEffect(() => {
// Initialize PlayCanvas
const app = new pc.Application(canvasRef.current);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
// Setup scene
setupScene(app);
app.start();
appRef.current = app;
// Cleanup
return () => {
app.destroy();
};
}, []);
return <canvas ref={canvasRef} />;
}
function setupScene(app) {
// Add camera, lights, objects...
}
export default PlayCanvasComponent;---
2. Three.js Migration
Migrate from Three.js concepts:
// Three.js
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// PlayCanvas equivalent
const entity = new pc.Entity('box');
entity.addComponent('model', { type: 'box' });
const material = new pc.StandardMaterial();
material.diffuse = new pc.Color(1, 0, 0);
material.update();
entity.model.material = material;
app.root.addChild(entity);---
3. WebXR Integration
VR/AR with WebXR:
// Enable XR
if (app.xr.supported) {
// VR mode
const enterVRButton = document.getElementById('enterVR');
enterVRButton.addEventListener('click', () => {
camera.camera.startXr(pc.XRTYPE_VR, pc.XRSPACE_LOCAL);
});
// AR mode
const enterARButton = document.getElementById('enterAR');
enterARButton.addEventListener('click', () => {
camera.camera.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR);
});
// XR input
app.xr.input.on('select', (inputSource) => {
console.log('XR input selected');
});
}---
Resources
- Official Examples: https://playcanvas.github.io
- API Docs: https://api.playcanvas.com
- Tutorials: https://developer.playcanvas.com/tutorials
- Forum: https://forum.playcanvas.com
---
License
Examples provided for educational purposes. PlayCanvas Engine is MIT licensed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>PlayCanvas Starter Project</title>
<link rel="stylesheet" href="styles.css">
<!-- PlayCanvas Engine -->
<script src="https://code.playcanvas.com/playcanvas-stable.min.js"></script>
</head>
<body>
<!-- Canvas Container -->
<div id="app">
<canvas id="application-canvas"></canvas>
<!-- Loading Screen -->
<div id="loading-screen">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<!-- UI Overlay -->
<div id="ui-overlay">
<div id="info-panel">
<h1>PlayCanvas Starter</h1>
<p>Use mouse to rotate camera • Scroll to zoom</p>
</div>
<div id="stats-panel">
<div class="stat">
<span class="label">FPS:</span>
<span id="fps">--</span>
</div>
<div class="stat">
<span class="label">Draw Calls:</span>
<span id="draw-calls">--</span>
</div>
<div class="stat">
<span class="label">Triangles:</span>
<span id="triangles">--</span>
</div>
</div>
<div id="controls-panel">
<button id="toggle-stats">Toggle Stats</button>
<button id="toggle-wireframe">Wireframe</button>
<button id="reset-camera">Reset Camera</button>
</div>
</div>
</div>
<!-- Application Scripts -->
<script src="scripts/input-manager.js"></script>
<script src="scripts/camera-controller.js"></script>
<script src="scripts/app.js"></script>
<script>
// Initialize application when DOM is ready
window.addEventListener('DOMContentLoaded', () => {
initApp();
});
</script>
</body>
</html>
PlayCanvas Starter Template
Production-ready PlayCanvas starter project with best practices, utilities, and interactive demo.
---
Features
✨ Complete Setup
- PlayCanvas engine integration
- Camera orbit controls
- Input management system
- Performance stats display
- Responsive UI overlay
- Mobile-friendly touch controls
🎨 Demo Scene
- 5 interactive 3D shapes
- Dynamic lighting (directional + ambient)
- PBR materials with varied metalness
- Smooth animations
- Ground plane with shadows
⚡ Performance
- Optimized rendering
- Real-time FPS, draw calls, triangle count
- Canvas auto-resizing
- Anti-aliasing enabled
📱 Responsive
- Desktop (mouse + keyboard)
- Mobile (touch + pinch-to-zoom)
- Tablet support
---
Quick Start
1. Download Template
# Copy starter template to your project
cp -r .claude/skills/playcanvas-engine/assets/starter_playcanvas ./my-playcanvas-project
cd my-playcanvas-project2. Serve Locally
Using Python:
python3 -m http.server 8000Using Node.js:
npx http-server -p 8000Using PHP:
php -S localhost:80003. Open in Browser
Navigate to http://localhost:8000
---
Project Structure
starter_playcanvas/
├── index.html # Main HTML file
├── styles.css # UI styling
├── scripts/
│ ├── app.js # Main application logic
│ ├── camera-controller.js # Orbit camera
│ └── input-manager.js # Input handling
└── README.md # This file---
Usage Guide
Camera Controls
Desktop:
- Left-click + drag: Rotate camera
- Right-click + drag: Rotate camera
- Scroll wheel: Zoom in/out
Mobile:
- Single finger drag: Rotate camera
- Pinch gesture: Zoom in/out
UI Controls
Toggle Stats: Show/hide performance statistics Wireframe: Toggle wireframe rendering (console only) Reset Camera: Return camera to initial position
---
Customization
Change Scene Background
Edit app.js:
camera.addComponent('camera', {
clearColor: new pc.Color(0.1, 0.1, 0.15), // RGB values 0-1
farClip: 100
});Add New Objects
// Create entity
const cube = new pc.Entity('MyCube');
// Add model component
cube.addComponent('model', {
type: 'box' // box, sphere, cylinder, cone, capsule, plane
});
// Position it
cube.setPosition(0, 2, 0);
// Create material
const material = new pc.StandardMaterial();
material.diffuse = new pc.Color(1, 0, 0); // Red
material.metalness = 0.5;
material.gloss = 0.7;
material.update();
cube.model.material = material;
// Add to scene
app.root.addChild(cube);Load 3D Models
// Load GLTF/GLB model
app.assets.loadFromUrl('model.glb', 'container', (err, asset) => {
if (err) {
console.error('Failed to load model:', err);
return;
}
const entity = asset.resource.instantiateRenderEntity();
entity.setPosition(0, 0, 0);
app.root.addChild(entity);
});Add Physics
// Add rigidbody component
entity.addComponent('rigidbody', {
type: 'dynamic', // dynamic, static, kinematic
mass: 1,
friction: 0.5,
restitution: 0.3
});
// Add collision component
entity.addComponent('collision', {
type: 'box' // box, sphere, capsule, cylinder, mesh
});Custom Scripts
Create script component:
var MyScript = pc.createScript('myScript');
MyScript.prototype.initialize = function() {
console.log('Script initialized');
};
MyScript.prototype.update = function(dt) {
// Update logic
this.entity.rotate(0, 10 * dt, 0);
};
// Attach to entity
entity.addComponent('script');
entity.script.create('myScript');---
Input Manager API
The included InputManager class provides centralized input handling:
// Access input manager (already instantiated in app.js)
const input = inputManager;
// Keyboard
if (input.isKeyPressed(pc.KEY_SPACE)) {
console.log('Space pressed');
}
// WASD input
const moveVector = input.getWASDInput(); // Returns Vec2
// Mouse
const mousePos = input.getMousePosition();
const mouseDelta = input.getMouseDelta();
const wheel = input.getMouseWheel();
// Raycast from mouse
const camera = app.root.findByName('Camera');
const ray = input.getMouseRay(camera);
if (ray) {
const result = input.raycast(ray.origin, ray.end);
if (result) {
console.log('Hit:', result.entity.name);
}
}
// Touch
const touchCount = input.getTouchCount();
const touch = input.getTouch(0); // First touch---
Camera Controller API
The CameraController class provides orbit camera functionality:
// Access camera controller (already instantiated in app.js)
const camera = cameraController;
// Set target
camera.setTarget(new pc.Vec3(0, 5, 0));
// Or focus on entity
const player = app.root.findByName('Player');
camera.focusOn(player, 10); // entity, distance
// Set rotation
camera.setYaw(45); // Horizontal rotation
camera.setPitch(30); // Vertical rotation
// Set distance
camera.setDistance(15);
// Animate camera
camera.animateTo(
90, // yaw
20, // pitch
12, // distance
2.0 // duration in seconds
);
// Reset to initial position
camera.reset();
// Get camera vectors
const forward = camera.getForwardVector();
const right = camera.getRightVector();
const up = camera.getUpVector();---
Performance Optimization
Reduce Draw Calls
// Batch static objects with same material
const material = new pc.StandardMaterial();
entities.forEach(entity => {
entity.model.material = material; // Share material
});Use LOD (Level of Detail)
// Add LOD levels
entity.addComponent('model', {
type: 'asset',
asset: highPolyModel
});
// Configure LOD
entity.model.meshInstances[0].lod = {
levels: [
{ distance: 10, mesh: highPolyMesh },
{ distance: 50, mesh: lowPolyMesh }
]
};Frustum Culling
Frustum culling is enabled by default. Ensure entities have proper bounding boxes:
// Update bounding box after scale changes
entity.model.meshInstances[0]._aabb.compute();Texture Compression
Use compressed texture formats for production:
- Desktop: DXT (DDS)
- iOS: PVR
- Android: ETC
- Universal: Basis
---
Deployment
Build for Production
1. Minify JavaScript:
# Using terser
npx terser scripts/app.js -o scripts/app.min.js
npx terser scripts/camera-controller.js -o scripts/camera-controller.min.js
npx terser scripts/input-manager.js -o scripts/input-manager.min.js2. Update index.html to use minified files
3. Compress Assets:
- Optimize textures (use tools like TinyPNG)
- Compress 3D models (use glTF-Pipeline)
- Enable gzip on server
Hosting Options
GitHub Pages:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin <your-repo-url>
git push -u origin main
# Enable GitHub Pages in repo settingsNetlify:
- Drag & drop project folder to Netlify
- Or connect GitHub repo for auto-deploy
Vercel:
npm i -g vercel
vercelAWS S3:
- Create S3 bucket
- Enable static website hosting
- Upload files
- Configure CloudFront CDN (optional)
---
Browser Compatibility
Supported Browsers:
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
WebGL Requirements:
- WebGL 2.0 (recommended)
- WebGL 1.0 (fallback)
Mobile:
- iOS Safari 14+
- Chrome Mobile 90+
- Samsung Internet 14+
---
Troubleshooting
Canvas not filling window:
// Ensure these are called
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);Assets not loading (CORS errors):
- Serve from local server (not file://)
- Check CORS headers on asset server
Poor performance on mobile:
// Reduce resolution for mobile
if (isMobile()) {
app.graphicsDevice.maxPixelRatio = 1;
}
// Disable shadows
light.light.castShadows = false;Touch controls not working:
// Ensure touch device is initialized
app.touch = new pc.TouchDevice(canvas);---
Next Steps
1. Add More Objects: Experiment with different shapes and materials 2. Load Models: Import your own 3D models (GLTF/GLB) 3. Add Physics: Create interactive physics simulations 4. Custom Scripts: Write gameplay logic with script components 5. UI Elements: Add 2D UI with element components 6. Particles: Create visual effects with particle systems 7. Audio: Add sound effects and music
---
Resources
- PlayCanvas Engine: https://github.com/playcanvas/engine
- API Documentation: https://api.playcanvas.com
- Examples: https://playcanvas.github.io
- Forum: https://forum.playcanvas.com
- Editor: https://playcanvas.com (for visual scene editing)
---
License
This starter template is provided as-is for educational and commercial use.
PlayCanvas Engine is licensed under MIT License.
---
Support
For issues or questions: 1. Check the PlayCanvas Forum 2. Review API documentation 3. Search examples
---
Happy coding! 🚀
/**
* PlayCanvas Starter Project
* Main application initialization and scene setup
*/
let app;
let cameraController;
let inputManager;
let statsEnabled = true;
let wireframeEnabled = false;
/**
* Initialize PlayCanvas application
*/
function initApp() {
const canvas = document.getElementById('application-canvas');
// Create PlayCanvas application
app = new pc.Application(canvas, {
mouse: new pc.Mouse(canvas),
touch: new pc.TouchDevice(canvas),
keyboard: new pc.Keyboard(window),
graphicsDeviceOptions: {
antialias: true,
alpha: false
}
});
// Configure application
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
// Start application
app.start();
// Setup scene
setupScene();
// Initialize managers
inputManager = new InputManager(app);
cameraController = new CameraController(app, app.root.findByName('Camera'));
// Setup UI controls
setupUI();
// Update loop
app.on('update', update);
// Hide loading screen
setTimeout(() => {
document.getElementById('loading-screen').classList.add('hidden');
}, 500);
// Handle window resize
window.addEventListener('resize', () => {
app.resizeCanvas();
});
console.log('PlayCanvas application initialized');
}
/**
* Setup scene with camera, lights, and objects
*/
function setupScene() {
// Camera
const camera = new pc.Entity('Camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.1, 0.1, 0.15),
farClip: 100
});
camera.setPosition(0, 5, 10);
camera.lookAt(0, 0, 0);
app.root.addChild(camera);
// Directional Light (Sun)
const light = new pc.Entity('DirectionalLight');
light.addComponent('light', {
type: 'directional',
color: new pc.Color(1, 1, 1),
intensity: 1,
castShadows: true,
shadowBias: 0.2,
shadowDistance: 40,
normalOffsetBias: 0.05
});
light.setEulerAngles(45, 30, 0);
app.root.addChild(light);
// Ambient Light
const ambient = new pc.Entity('AmbientLight');
ambient.addComponent('light', {
type: 'directional',
color: new pc.Color(0.4, 0.5, 0.6),
intensity: 0.3
});
ambient.setEulerAngles(-45, 0, 0);
app.root.addChild(ambient);
// Ground Plane
const ground = new pc.Entity('Ground');
ground.addComponent('model', {
type: 'plane'
});
ground.setLocalScale(20, 1, 20);
// Ground material
const groundMaterial = new pc.StandardMaterial();
groundMaterial.diffuse = new pc.Color(0.3, 0.3, 0.35);
groundMaterial.metalness = 0.0;
groundMaterial.gloss = 0.3;
groundMaterial.update();
ground.model.material = groundMaterial;
app.root.addChild(ground);
// Create demo objects
createDemoObjects();
}
/**
* Create demo objects in the scene
*/
function createDemoObjects() {
const colors = [
new pc.Color(0.8, 0.3, 0.3), // Red
new pc.Color(0.3, 0.8, 0.3), // Green
new pc.Color(0.3, 0.3, 0.8), // Blue
new pc.Color(0.8, 0.8, 0.3), // Yellow
new pc.Color(0.8, 0.3, 0.8) // Magenta
];
const shapes = ['box', 'sphere', 'cylinder', 'cone', 'capsule'];
for (let i = 0; i < 5; i++) {
const entity = new pc.Entity(`Shape_${i}`);
// Add model component
entity.addComponent('model', {
type: shapes[i]
});
// Position in a circle
const angle = (i / 5) * Math.PI * 2;
const radius = 4;
entity.setPosition(
Math.cos(angle) * radius,
1,
Math.sin(angle) * radius
);
// Create material
const material = new pc.StandardMaterial();
material.diffuse = colors[i];
material.metalness = 0.2 + (i * 0.15);
material.gloss = 0.7;
material.update();
entity.model.material = material;
// Store rotation speed
entity.rotationSpeed = new pc.Vec3(
10 + i * 5,
20 + i * 5,
15 + i * 5
);
app.root.addChild(entity);
}
}
/**
* Main update loop
*/
function update(dt) {
// Update camera controller
if (cameraController) {
cameraController.update(dt);
}
// Rotate demo objects
const shapes = app.root.find((node) => node.name.startsWith('Shape_'));
shapes.forEach((shape) => {
if (shape.rotationSpeed) {
shape.rotate(
shape.rotationSpeed.x * dt,
shape.rotationSpeed.y * dt,
shape.rotationSpeed.z * dt
);
}
});
// Update stats
if (statsEnabled) {
updateStats();
}
}
/**
* Update performance stats display
*/
function updateStats() {
const stats = app.stats;
document.getElementById('fps').textContent = Math.round(1 / app.dt);
document.getElementById('draw-calls').textContent = stats.drawCalls.total;
document.getElementById('triangles').textContent = Math.round(stats.misc.tricount / 1000) + 'k';
}
/**
* Setup UI controls
*/
function setupUI() {
// Toggle stats
document.getElementById('toggle-stats').addEventListener('click', () => {
statsEnabled = !statsEnabled;
document.getElementById('stats-panel').classList.toggle('hidden');
});
// Toggle wireframe
document.getElementById('toggle-wireframe').addEventListener('click', () => {
wireframeEnabled = !wireframeEnabled;
// Update all materials
const entities = app.root.find((node) => node.model);
entities.forEach((entity) => {
if (entity.model && entity.model.material) {
// Create new material instance if needed
if (!entity.model.material.wireframe) {
const material = entity.model.material;
material.update();
}
}
});
// Note: Wireframe rendering requires custom shader in PlayCanvas
console.log('Wireframe mode:', wireframeEnabled);
});
// Reset camera
document.getElementById('reset-camera').addEventListener('click', () => {
if (cameraController) {
cameraController.reset();
}
});
}
/**
* Cleanup resources
*/
function cleanup() {
if (app) {
app.destroy();
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', cleanup);
/**
* Camera Controller
* Orbit camera with mouse/touch controls
*/
class CameraController {
constructor(app, cameraEntity, options = {}) {
this.app = app;
this.camera = cameraEntity;
// Configuration
this.target = options.target || new pc.Vec3(0, 0, 0);
this.distance = options.distance || 10;
this.minDistance = options.minDistance || 2;
this.maxDistance = options.maxDistance || 50;
this.sensitivity = options.sensitivity || 0.3;
this.damping = options.damping || 0.15;
this.zoomSpeed = options.zoomSpeed || 0.5;
// State
this.yaw = options.initialYaw || 0;
this.pitch = options.initialPitch || 20;
this.targetYaw = this.yaw;
this.targetPitch = this.pitch;
this.currentDistance = this.distance;
this.targetDistance = this.distance;
// Input tracking
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
// Touch tracking
this.lastTouchDistance = 0;
// Initial camera position
this.initialYaw = this.yaw;
this.initialPitch = this.pitch;
this.initialDistance = this.distance;
this.init();
}
init() {
const canvas = this.app.graphicsDevice.canvas;
// Mouse events
if (this.app.mouse) {
canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
canvas.addEventListener('wheel', this.onMouseWheel.bind(this));
}
// Touch events
if (this.app.touch) {
canvas.addEventListener('touchstart', this.onTouchStart.bind(this));
canvas.addEventListener('touchmove', this.onTouchMove.bind(this));
canvas.addEventListener('touchend', this.onTouchEnd.bind(this));
}
// Prevent context menu on right-click
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
// Mouse Events
onMouseDown(event) {
if (event.button === 0 || event.button === 2) { // Left or right button
this.isDragging = true;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
}
}
onMouseMove(event) {
if (this.isDragging) {
const deltaX = event.clientX - this.lastMouseX;
const deltaY = event.clientY - this.lastMouseY;
this.targetYaw -= deltaX * this.sensitivity;
this.targetPitch -= deltaY * this.sensitivity;
// Clamp pitch
this.targetPitch = Math.max(-89, Math.min(89, this.targetPitch));
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
}
}
onMouseUp(event) {
if (event.button === 0 || event.button === 2) {
this.isDragging = false;
}
}
onMouseWheel(event) {
event.preventDefault();
const delta = event.deltaY > 0 ? 1 : -1;
this.targetDistance += delta * this.zoomSpeed;
this.targetDistance = Math.max(
this.minDistance,
Math.min(this.maxDistance, this.targetDistance)
);
}
// Touch Events
onTouchStart(event) {
if (event.touches.length === 1) {
// Single touch - rotate
this.isDragging = true;
this.lastMouseX = event.touches[0].clientX;
this.lastMouseY = event.touches[0].clientY;
} else if (event.touches.length === 2) {
// Two finger touch - zoom
this.lastTouchDistance = this.getTouchDistance(event.touches);
}
}
onTouchMove(event) {
event.preventDefault();
if (event.touches.length === 1 && this.isDragging) {
// Single touch - rotate
const deltaX = event.touches[0].clientX - this.lastMouseX;
const deltaY = event.touches[0].clientY - this.lastMouseY;
this.targetYaw -= deltaX * this.sensitivity;
this.targetPitch -= deltaY * this.sensitivity;
this.targetPitch = Math.max(-89, Math.min(89, this.targetPitch));
this.lastMouseX = event.touches[0].clientX;
this.lastMouseY = event.touches[0].clientY;
} else if (event.touches.length === 2) {
// Two finger touch - zoom
const distance = this.getTouchDistance(event.touches);
const delta = this.lastTouchDistance - distance;
this.targetDistance += delta * 0.01;
this.targetDistance = Math.max(
this.minDistance,
Math.min(this.maxDistance, this.targetDistance)
);
this.lastTouchDistance = distance;
}
}
onTouchEnd(event) {
if (event.touches.length === 0) {
this.isDragging = false;
} else if (event.touches.length === 2) {
this.lastTouchDistance = this.getTouchDistance(event.touches);
}
}
getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
// Update Loop
update(dt) {
if (!this.camera) return;
// Smooth damping
this.yaw += (this.targetYaw - this.yaw) * this.damping;
this.pitch += (this.targetPitch - this.pitch) * this.damping;
this.currentDistance += (this.targetDistance - this.currentDistance) * this.damping;
// Calculate camera position
const yawRad = this.yaw * pc.math.DEG_TO_RAD;
const pitchRad = this.pitch * pc.math.DEG_TO_RAD;
const x = this.target.x + this.currentDistance * Math.cos(pitchRad) * Math.sin(yawRad);
const y = this.target.y + this.currentDistance * Math.sin(pitchRad);
const z = this.target.z + this.currentDistance * Math.cos(pitchRad) * Math.cos(yawRad);
this.camera.setPosition(x, y, z);
this.camera.lookAt(this.target);
}
// Control Methods
setTarget(target) {
if (target instanceof pc.Vec3) {
this.target.copy(target);
} else if (target instanceof pc.Entity) {
this.target.copy(target.getPosition());
}
}
setDistance(distance) {
this.targetDistance = Math.max(
this.minDistance,
Math.min(this.maxDistance, distance)
);
}
setYaw(yaw) {
this.targetYaw = yaw;
}
setPitch(pitch) {
this.targetPitch = Math.max(-89, Math.min(89, pitch));
}
reset() {
this.targetYaw = this.initialYaw;
this.targetPitch = this.initialPitch;
this.targetDistance = this.initialDistance;
}
focusOn(entity, distance = null) {
if (entity instanceof pc.Entity) {
this.setTarget(entity.getPosition());
} else if (entity instanceof pc.Vec3) {
this.setTarget(entity);
}
if (distance !== null) {
this.setDistance(distance);
}
}
// Animation
animateTo(yaw, pitch, distance, duration = 1.0) {
// Simple tween implementation
const startYaw = this.yaw;
const startPitch = this.pitch;
const startDistance = this.currentDistance;
const startTime = Date.now();
const animate = () => {
const elapsed = (Date.now() - startTime) / 1000;
const t = Math.min(elapsed / duration, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - t, 3);
this.targetYaw = startYaw + (yaw - startYaw) * eased;
this.targetPitch = startPitch + (pitch - startPitch) * eased;
this.targetDistance = startDistance + (distance - startDistance) * eased;
if (t < 1) {
requestAnimationFrame(animate);
}
};
animate();
}
// Utility
getForwardVector() {
return this.camera.forward.clone();
}
getRightVector() {
return this.camera.right.clone();
}
getUpVector() {
return this.camera.up.clone();
}
// Cleanup
destroy() {
// Remove event listeners if needed
this.camera = null;
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = CameraController;
}
/**
* Input Manager
* Centralized input handling for keyboard, mouse, and touch
*/
class InputManager {
constructor(app) {
this.app = app;
this.keyboard = app.keyboard;
this.mouse = app.mouse;
this.touch = app.touch;
// Input state
this.keys = {};
this.mouseButtons = {};
this.mousePosition = new pc.Vec2();
this.mouseDelta = new pc.Vec2();
this.mouseWheel = 0;
// Touch state
this.touches = [];
this.touchCount = 0;
this.init();
}
init() {
// Keyboard events
if (this.keyboard) {
this.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown.bind(this));
this.keyboard.on(pc.EVENT_KEYUP, this.onKeyUp.bind(this));
}
// Mouse events
if (this.mouse) {
this.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown.bind(this));
this.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp.bind(this));
this.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove.bind(this));
this.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel.bind(this));
}
// Touch events
if (this.touch) {
this.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStart.bind(this));
this.touch.on(pc.EVENT_TOUCHEND, this.onTouchEnd.bind(this));
this.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove.bind(this));
this.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchCancel.bind(this));
}
}
// Keyboard Methods
onKeyDown(event) {
this.keys[event.key] = true;
}
onKeyUp(event) {
this.keys[event.key] = false;
}
isKeyPressed(key) {
return this.keys[key] === true;
}
wasKeyPressed(key) {
if (this.keyboard) {
return this.keyboard.wasPressed(key);
}
return false;
}
wasKeyReleased(key) {
if (this.keyboard) {
return this.keyboard.wasReleased(key);
}
return false;
}
// Mouse Methods
onMouseDown(event) {
this.mouseButtons[event.button] = true;
}
onMouseUp(event) {
this.mouseButtons[event.button] = false;
}
onMouseMove(event) {
this.mousePosition.set(event.x, event.y);
this.mouseDelta.set(event.dx, event.dy);
}
onMouseWheel(event) {
this.mouseWheel = event.wheel;
}
isMouseButtonPressed(button) {
return this.mouseButtons[button] === true;
}
getMousePosition() {
if (this.mouse) {
return new pc.Vec2(this.mouse.x, this.mouse.y);
}
return this.mousePosition.clone();
}
getMouseDelta() {
return this.mouseDelta.clone();
}
getMouseWheel() {
return this.mouseWheel;
}
// Touch Methods
onTouchStart(event) {
this.updateTouches(event);
}
onTouchEnd(event) {
this.updateTouches(event);
}
onTouchMove(event) {
this.updateTouches(event);
}
onTouchCancel(event) {
this.touches = [];
this.touchCount = 0;
}
updateTouches(event) {
if (!this.touch) return;
this.touches = [];
this.touchCount = event.touches.length;
for (let i = 0; i < event.touches.length; i++) {
const touch = event.touches[i];
this.touches.push({
id: touch.id,
x: touch.x,
y: touch.y,
dx: touch.dx || 0,
dy: touch.dy || 0
});
}
}
getTouchCount() {
return this.touchCount;
}
getTouch(index) {
if (index >= 0 && index < this.touches.length) {
return this.touches[index];
}
return null;
}
getTouches() {
return this.touches;
}
// Utility Methods
getInputVector(upKey, downKey, leftKey, rightKey) {
const vector = new pc.Vec2(0, 0);
if (this.isKeyPressed(upKey)) vector.y += 1;
if (this.isKeyPressed(downKey)) vector.y -= 1;
if (this.isKeyPressed(leftKey)) vector.x -= 1;
if (this.isKeyPressed(rightKey)) vector.x += 1;
// Normalize diagonal movement
if (vector.length() > 0) {
vector.normalize();
}
return vector;
}
getWASDInput() {
return this.getInputVector(
pc.KEY_W,
pc.KEY_S,
pc.KEY_A,
pc.KEY_D
);
}
getArrowKeysInput() {
return this.getInputVector(
pc.KEY_UP,
pc.KEY_DOWN,
pc.KEY_LEFT,
pc.KEY_RIGHT
);
}
// Raycast from screen position
screenToWorldRay(screenX, screenY, camera) {
if (!camera || !camera.camera) return null;
const worldPos = camera.camera.screenToWorld(
screenX,
screenY,
camera.camera.farClip
);
return {
origin: camera.getPosition(),
direction: worldPos.clone().sub(camera.getPosition()).normalize(),
end: worldPos
};
}
// Get raycast from mouse
getMouseRay(camera) {
if (!this.mouse || !camera) return null;
return this.screenToWorldRay(
this.mouse.x,
this.mouse.y,
camera
);
}
// Perform raycast
raycast(from, to) {
return this.app.systems.rigidbody.raycastFirst(from, to);
}
raycastAll(from, to) {
return this.app.systems.rigidbody.raycastAll(from, to);
}
// Cleanup
destroy() {
this.keys = {};
this.mouseButtons = {};
this.touches = [];
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = InputManager;
}
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #000;
color: #fff;
}
/* App Container */
#app {
position: relative;
width: 100%;
height: 100%;
}
/* Canvas */
#application-canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
/* Loading Screen */
#loading-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
z-index: 1000;
transition: opacity 0.5s ease;
}
#loading-screen.hidden {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#loading-screen p {
margin-top: 20px;
font-size: 18px;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.8;
}
/* UI Overlay */
#ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
#ui-overlay > * {
pointer-events: auto;
}
/* Info Panel */
#info-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#info-panel h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#info-panel p {
font-size: 14px;
opacity: 0.8;
line-height: 1.5;
}
/* Stats Panel */
#stats-panel {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
min-width: 180px;
}
#stats-panel.hidden {
display: none;
}
.stat {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.stat:last-child {
margin-bottom: 0;
}
.stat .label {
opacity: 0.7;
margin-right: 15px;
}
.stat span:last-child {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* Controls Panel */
#controls-panel {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
}
button {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
/* Mobile Responsive */
@media (max-width: 768px) {
#info-panel {
top: 10px;
left: 10px;
padding: 15px;
}
#info-panel h1 {
font-size: 20px;
}
#info-panel p {
font-size: 12px;
}
#stats-panel {
top: 10px;
right: 10px;
padding: 10px;
min-width: 150px;
font-size: 12px;
}
#controls-panel {
bottom: 10px;
flex-direction: column;
gap: 8px;
}
button {
padding: 10px 20px;
font-size: 13px;
}
}
/* Performance Indicator */
.performance-warning {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 100, 100, 0.9);
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 100;
}
.performance-warning.hidden {
display: none;
}
PlayCanvas Engine API Reference
Complete API documentation for the PlayCanvas WebGL/WebGPU engine.
Version: 1.70+ License: MIT Official Docs: https://api.playcanvas.com/
---
Table of Contents
1. Application 2. Entity 3. Components 4. Assets 5. Graphics 6. Input 7. Physics 8. Audio
---
Application
pc.Application
The root application managing the rendering loop, scene, and systems.
Constructor
const app = new pc.Application(canvas, options);Parameters:
canvas(HTMLCanvasElement): Canvas element for renderingoptions(Object): Configuration optionskeyboard(pc.Keyboard): Keyboard devicemouse(pc.Mouse): Mouse devicetouch(pc.TouchDevice): Touch devicegamepads(pc.GamePads): Gamepad devicesgraphicsDeviceOptions(Object): WebGL/WebGPU settings
Properties
| Property | Type | Description |
|---|---|---|
root | pc.Entity | Root entity of scene hierarchy |
scene | pc.Scene | Scene settings |
assets | pc.AssetRegistry | Asset management |
graphicsDevice | pc.GraphicsDevice | Rendering device |
systems | pc.ComponentSystemRegistry | Component systems |
keyboard | pc.Keyboard | Keyboard input |
mouse | pc.Mouse | Mouse input |
touch | pc.TouchDevice | Touch input |
gamepads | pc.GamePads | Gamepad input |
timeScale | Number | Global time scale (default: 1) |
Methods
setCanvasFillMode(mode)
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);Modes:
pc.FILLMODE_NONE: No auto-resizepc.FILLMODE_FILL_WINDOW: Fill entire windowpc.FILLMODE_KEEP_ASPECT: Maintain aspect ratio
setCanvasResolution(mode)
app.setCanvasResolution(pc.RESOLUTION_AUTO);Modes:
pc.RESOLUTION_AUTO: Match window device pixel ratiopc.RESOLUTION_FIXED: Use canvas width/height
resizeCanvas()
app.resizeCanvas();
// Manually trigger resizestart()
app.start();
// Start the application update loopdestroy()
app.destroy();
// Clean up application resourcesEvents
app.on('update', (dt) => {
// Called every frame
// dt = delta time in seconds
});
app.on('postUpdate', (dt) => {
// Called after update
});
app.on('prerender', () => {
// Before rendering
});
app.on('postrender', () => {
// After rendering
});
app.on('start', () => {
// Application started
});
app.on('destroy', () => {
// Application destroyed
});---
Entity
pc.Entity
Nodes in the scene hierarchy with components.
Constructor
const entity = new pc.Entity(name);Properties
| Property | Type | Description |
|---|---|---|
name | String | Entity name |
enabled | Boolean | Render and update enabled |
children | Array | Child entities |
parent | pc.Entity | Parent entity |
tags | pc.Tags | Tag list for searching |
Component Accessors:
entity.model- Model componententity.camera- Camera componententity.light- Light componententity.script- Script componententity.rigidbody- Rigidbody componententity.collision- Collision componententity.animation- Animation componententity.sound- Sound component
Transform Methods
Position:
entity.setPosition(x, y, z);
entity.setPosition(new pc.Vec3(x, y, z));
entity.setLocalPosition(x, y, z);
const pos = entity.getPosition(); // World position
const localPos = entity.getLocalPosition(); // Local position
entity.translate(x, y, z); // Move relative
entity.translateLocal(x, y, z); // Move in local spaceRotation:
entity.setEulerAngles(x, y, z); // Degrees
entity.setLocalEulerAngles(x, y, z);
const angles = entity.getEulerAngles();
const localAngles = entity.getLocalEulerAngles();
entity.rotate(x, y, z); // Rotate relative (degrees)
entity.rotateLocal(x, y, z);
// Quaternion rotation
entity.setRotation(quat);
const quat = entity.getRotation();
// Look at target
entity.lookAt(target); // target is Vec3 or Entity
entity.lookAt(x, y, z);Scale:
entity.setLocalScale(x, y, z);
entity.setLocalScale(new pc.Vec3(x, y, z));
const scale = entity.getLocalScale();Forward/Right/Up Vectors:
const forward = entity.forward; // pc.Vec3
const right = entity.right;
const up = entity.up;Hierarchy Methods
addChild(entity)
parent.addChild(child);removeChild(entity)
parent.removeChild(child);insertChild(entity, index)
parent.insertChild(child, 0); // Insert at startreparent(parent)
entity.reparent(newParent);find()
// Find by name
const child = entity.find(name => name === 'PlayerModel');
// Find all
const allChildren = entity.find(() => true);findByName(name)
const player = app.root.findByName('Player');findByTag(tag)
const enemies = app.root.findByTag('enemy');
// Returns array of entitiesComponent Methods
addComponent(type, data)
entity.addComponent('model', {
type: 'box'
});removeComponent(type)
entity.removeComponent('model');hasComponent(type)
if (entity.hasComponent('rigidbody')) {
// Has physics
}Entity Methods
clone()
const clone = entity.clone();
app.root.addChild(clone);destroy()
entity.destroy();
// Removes from parent and cleans upenable()/disable()
entity.enabled = false; // Disable
entity.enabled = true; // Enable---
Components
Model Component
Renders 3D meshes.
entity.addComponent('model', {
type: 'box', // Primitive type
asset: assetId, // Model asset
castShadows: true,
receiveShadows: true,
castShadowsLightmap: false,
lightmapped: false,
isStatic: false,
layers: [pc.LAYERID_WORLD]
});Primitive Types:
'box','capsule','cone','cylinder','plane','sphere'
Properties:
entity.model.meshInstances; // Array of MeshInstance
entity.model.material = material;
entity.model.asset = assetId;---
Camera Component
Renders the scene from a viewpoint.
entity.addComponent('camera', {
clearColor: new pc.Color(0, 0, 0, 1),
fov: 45,
aspectRatio: 16/9, // Auto-calculated if null
nearClip: 0.1,
farClip: 1000,
projection: pc.PROJECTION_PERSPECTIVE,
priority: 0,
frustumCulling: true,
rect: new pc.Vec4(0, 0, 1, 1), // Viewport
layers: [pc.LAYERID_WORLD, pc.LAYERID_UI]
});Projection Types:
pc.PROJECTION_PERSPECTIVEpc.PROJECTION_ORTHOGRAPHIC
Methods:
// Screen to world conversion
const worldPos = camera.camera.screenToWorld(screenX, screenY, depth);
// World to screen
const screenPos = camera.camera.worldToScreen(worldPos);
// Ray from screen point
const ray = camera.camera.screenToWorld(screenX, screenY, camera.camera.nearClip);---
Light Component
Illuminates the scene.
entity.addComponent('light', {
type: pc.LIGHTTYPE_DIRECTIONAL,
color: new pc.Color(1, 1, 1),
intensity: 1,
castShadows: true,
shadowDistance: 40,
shadowResolution: 2048,
shadowBias: 0.05,
normalOffsetBias: 0.05,
range: 10, // Point/Spot only
innerConeAngle: 40, // Spot only
outerConeAngle: 45, // Spot only
falloffMode: pc.LIGHTFALLOFF_INVERSESQUARED,
layers: [pc.LAYERID_WORLD]
});Light Types:
pc.LIGHTTYPE_DIRECTIONAL: Sun-like, parallel rayspc.LIGHTTYPE_POINT: Omnidirectional, like a bulbpc.LIGHTTYPE_SPOT: Cone-shaped, like a flashlight
Shadow Types:
entity.light.shadowType = pc.SHADOW_PCF3;pc.SHADOW_PCF3: 3x3 PCF (good quality)pc.SHADOW_PCF5: 5x5 PCF (better quality, slower)pc.SHADOW_VSM8: Variance shadow maps (softer)
---
Rigidbody Component
Adds physics simulation.
entity.addComponent('rigidbody', {
type: pc.BODYTYPE_DYNAMIC,
mass: 1,
linearDamping: 0,
angularDamping: 0,
linearFactor: new pc.Vec3(1, 1, 1),
angularFactor: new pc.Vec3(1, 1, 1),
friction: 0.5,
restitution: 0,
group: pc.BODYGROUP_DYNAMIC,
mask: pc.BODYMASK_ALL
});Body Types:
pc.BODYTYPE_STATIC: Immovable (terrain, buildings)pc.BODYTYPE_DYNAMIC: Affected by forcespc.BODYTYPE_KINEMATIC: Moved by script, not physics
Methods:
// Apply force
entity.rigidbody.applyForce(x, y, z);
entity.rigidbody.applyForce(new pc.Vec3(x, y, z));
// Apply force at point
entity.rigidbody.applyForce(force, point);
// Apply impulse (instant velocity change)
entity.rigidbody.applyImpulse(x, y, z);
entity.rigidbody.applyImpulse(impulse, point);
// Apply torque
entity.rigidbody.applyTorque(x, y, z);
// Apply torque impulse
entity.rigidbody.applyTorqueImpulse(x, y, z);
// Velocity
entity.rigidbody.linearVelocity = new pc.Vec3(x, y, z);
entity.rigidbody.angularVelocity = new pc.Vec3(x, y, z);
// Teleport
entity.rigidbody.teleport(x, y, z);
entity.rigidbody.teleport(position, rotation);---
Collision Component
Defines physics collision shape.
entity.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(0.5, 0.5, 0.5),
radius: 0.5, // Sphere/Capsule
axis: pc.AXIS_Y, // Capsule/Cylinder
height: 2, // Capsule/Cylinder
asset: assetId, // Mesh collision
renderAsset: assetId // Mesh collision (render)
});Collision Types:
'box': Box shape'sphere': Sphere shape'capsule': Capsule shape'cylinder': Cylinder shape'cone': Cone shape'mesh': Triangle mesh (static only)'compound': Multiple shapes
Events:
entity.collision.on('collisionstart', (result) => {
console.log('Collision with:', result.other.name);
console.log('Contact point:', result.contacts[0].point);
});
entity.collision.on('collisionend', (other) => {
console.log('Collision ended with:', other.name);
});
entity.collision.on('contact', (result) => {
// Contact maintained
});---
Script Component
Runs custom JavaScript code.
entity.addComponent('script');
entity.script.create('scriptName', {
attributes: {
speed: 10,
target: targetEntity
}
});Script Definition:
const MyScript = pc.createScript('myScript');
MyScript.attributes.add('speed', {
type: 'number',
default: 10,
title: 'Movement Speed',
description: 'Units per second'
});
MyScript.attributes.add('target', {
type: 'entity',
title: 'Target Entity'
});
MyScript.prototype.initialize = function() {
// Called once
};
MyScript.prototype.update = function(dt) {
// Called every frame
this.entity.translate(0, 0, this.speed * dt);
};
MyScript.prototype.postUpdate = function(dt) {
// After all updates
};
MyScript.prototype.destroy = function() {
// Cleanup
};Attribute Types:
'boolean','number','string''entity','asset','rgb','rgba''vec2','vec3','vec4''curve','colorcurve''json'
---
Animation Component
Plays skeletal animations.
entity.addComponent('animation', {
assets: [animAsset],
speed: 1.0,
loop: true,
activate: true
});Methods:
// Play animation
entity.animation.play('Run', 0.2); // 0.2s blend time
// Get current animations
const anims = entity.animation.animations;
// Animation events
entity.animation.on('animationend', (name) => {
console.log('Animation ended:', name);
});---
Sound Component
3D positional audio.
entity.addComponent('sound', {
positional: true,
refDistance: 1,
maxDistance: 10000,
rollOffFactor: 1,
distanceModel: pc.DISTANCE_LINEAR,
slots: {
'music': {
asset: musicAsset,
autoPlay: true,
loop: true,
volume: 0.5,
pitch: 1.0
}
}
});Methods:
entity.sound.play('slotName');
entity.sound.pause('slotName');
entity.sound.stop('slotName');
entity.sound.volume = 0.8; // Global volume---
Assets
pc.AssetRegistry
Manages loading and caching of assets.
add(asset)
const asset = new pc.Asset('name', 'texture', { url: '/texture.jpg' });
app.assets.add(asset);load(asset)
app.assets.load(asset);remove(asset)
app.assets.remove(asset);find()
// Find by name
const asset = app.assets.find('PlayerModel');
// Find by type
const textures = app.assets.findAll('texture');
// Find by tag
const tagged = app.assets.findByTag('environment');Events:
asset.ready((loadedAsset) => {
// Asset loaded
console.log('Loaded:', loadedAsset.resource);
});
asset.on('load', (asset) => {
// Asset loaded
});
asset.on('error', (err) => {
// Loading failed
});
asset.on('remove', () => {
// Asset removed from registry
});---
Asset Types
Texture:
const texture = new pc.Asset('diffuse', 'texture', {
url: '/textures/diffuse.jpg'
});Model (Container):
const model = new pc.Asset('character', 'container', {
url: '/models/character.glb'
});
model.ready((asset) => {
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
});Material:
const material = new pc.Asset('custom', 'material');Audio:
const audio = new pc.Asset('music', 'audio', {
url: '/audio/music.mp3'
});Script:
const script = new pc.Asset('playerController', 'script', {
url: '/scripts/player-controller.js'
});---
Graphics
pc.StandardMaterial
PBR material for rendering.
const material = new pc.StandardMaterial();
// Diffuse (albedo)
material.diffuse = new pc.Color(1, 0, 0); // Red
material.diffuseMap = texture;
// Metalness
material.metalness = 0.5;
material.metalnessMap = metalnessTexture;
// Gloss (inverse of roughness)
material.gloss = 0.8;
material.glossMap = roughnessTexture;
// Normal map
material.normalMap = normalTexture;
material.bumpiness = 1.0;
// Emissive
material.emissive = new pc.Color(1, 1, 0);
material.emissiveMap = emissiveTexture;
material.emissiveIntensity = 1.0;
// Ambient occlusion
material.aoMap = aoTexture;
// Opacity
material.opacity = 0.5;
material.opacityMap = opacityTexture;
material.blendType = pc.BLEND_NORMAL;
// Update material
material.update();Blend Types:
pc.BLEND_NONE: Opaquepc.BLEND_NORMAL: Alpha blendingpc.BLEND_ADDITIVE: Additive blendingpc.BLEND_MULTIPLICATIVE: Multiply blending
---
pc.Texture
2D texture resource.
const texture = new pc.Texture(app.graphicsDevice, {
width: 512,
height: 512,
format: pc.PIXELFORMAT_RGBA8,
minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR,
magFilter: pc.FILTER_LINEAR,
addressU: pc.ADDRESS_REPEAT,
addressV: pc.ADDRESS_REPEAT,
mipmaps: true,
anisotropy: 16
});Pixel Formats:
pc.PIXELFORMAT_RGBA8: Standard 8-bit RGBApc.PIXELFORMAT_RGB8: 8-bit RGBpc.PIXELFORMAT_DXT5: GPU-compressedpc.PIXELFORMAT_ETC2_RGBA: Mobile compression
Filters:
pc.FILTER_NEAREST: Pixelatedpc.FILTER_LINEAR: Smoothpc.FILTER_LINEAR_MIPMAP_LINEAR: Trilinear (best quality)
Address Modes:
pc.ADDRESS_REPEAT: Tile texturepc.ADDRESS_CLAMP: Clamp to edgepc.ADDRESS_MIRRORED_REPEAT: Mirror tiling
---
Input
pc.Keyboard
Keyboard input handling.
const keyboard = new pc.Keyboard(window);
// Check if key is currently pressed
if (keyboard.isPressed(pc.KEY_W)) {
player.translate(0, 0, -speed * dt);
}
// Check if key was just pressed this frame
if (keyboard.wasPressed(pc.KEY_SPACE)) {
player.jump();
}
// Check if key was just released
if (keyboard.wasReleased(pc.KEY_SHIFT)) {
player.stopSprinting();
}
// Events
keyboard.on(pc.EVENT_KEYDOWN, (event) => {
console.log('Key down:', event.key);
});
keyboard.on(pc.EVENT_KEYUP, (event) => {
console.log('Key up:', event.key);
});Key Constants:
pc.KEY_W,pc.KEY_A,pc.KEY_S,pc.KEY_Dpc.KEY_SPACE,pc.KEY_SHIFT,pc.KEY_CONTROLpc.KEY_ENTER,pc.KEY_ESCAPEpc.KEY_UP,pc.KEY_DOWN,pc.KEY_LEFT,pc.KEY_RIGHTpc.KEY_0throughpc.KEY_9pc.KEY_Athroughpc.KEY_Z
---
pc.Mouse
Mouse input handling.
const mouse = new pc.Mouse(canvas);
// Events
mouse.on(pc.EVENT_MOUSEDOWN, (event) => {
if (event.button === pc.MOUSEBUTTON_LEFT) {
console.log('Left click at:', event.x, event.y);
}
});
mouse.on(pc.EVENT_MOUSEUP, (event) => {
console.log('Mouse up');
});
mouse.on(pc.EVENT_MOUSEMOVE, (event) => {
const dx = event.dx; // Delta movement
const dy = event.dy;
camera.rotate(-dy * 0.2, -dx * 0.2, 0);
});
mouse.on(pc.EVENT_MOUSEWHEEL, (event) => {
zoom += event.wheelDelta * 0.1;
});Mouse Buttons:
pc.MOUSEBUTTON_LEFT: Left buttonpc.MOUSEBUTTON_MIDDLE: Middle buttonpc.MOUSEBUTTON_RIGHT: Right button
Properties:
mouse.isPressed(pc.MOUSEBUTTON_LEFT); // Check if button pressed---
pc.TouchDevice
Touch input for mobile.
const touch = new pc.TouchDevice(canvas);
touch.on(pc.EVENT_TOUCHSTART, (event) => {
event.touches.forEach((touch, index) => {
console.log(`Touch ${index} at:`, touch.x, touch.y);
});
});
touch.on(pc.EVENT_TOUCHEND, (event) => {
// Touches ended
});
touch.on(pc.EVENT_TOUCHMOVE, (event) => {
const touch = event.touches[0];
const dx = touch.dx;
const dy = touch.dy;
});---
Physics
Raycasting
Cast rays to detect collisions.
// Raycast from camera through mouse
const camera = app.root.findByName('Camera');
const from = camera.camera.screenToWorld(mouseX, mouseY, camera.camera.nearClip);
const to = camera.camera.screenToWorld(mouseX, mouseY, camera.camera.farClip);
const result = app.systems.rigidbody.raycastFirst(from, to);
if (result) {
console.log('Hit entity:', result.entity.name);
console.log('Hit point:', result.point);
console.log('Hit normal:', result.normal);
}
// Raycast all
const results = app.systems.rigidbody.raycastAll(from, to);
results.forEach(result => {
console.log('Hit:', result.entity.name);
});---
Audio
pc.SoundComponent
3D positional audio.
entity.addComponent('sound');
// Add sound slot
entity.sound.addSlot('footstep', {
asset: footstepAsset,
autoPlay: false,
loop: false,
volume: 0.8,
pitch: 1.0,
positional: true,
refDistance: 1,
maxDistance: 20
});
// Play sound
entity.sound.play('footstep');
// Stop sound
entity.sound.stop('footstep');
// Pause sound
entity.sound.pause('footstep');
// Resume
entity.sound.resume('footstep');---
Utilities
pc.Vec3
3D vector.
const v = new pc.Vec3(x, y, z);
// Operations
v.add(other);
v.sub(other);
v.mul(scalar);
v.div(scalar);
v.dot(other);
v.cross(other);
v.normalize();
v.length();
v.distance(other);
v.lerp(a, b, t);pc.Color
RGBA color.
const color = new pc.Color(r, g, b, a); // 0-1 range
// Conversion
const hex = color.toString(); // "#RRGGBB"pc.Quat
Quaternion rotation.
const quat = new pc.Quat();
quat.setFromEulerAngles(x, y, z);
quat.slerp(a, b, t); // Spherical interpolation---
Constants Reference
Fill Modes:
pc.FILLMODE_NONEpc.FILLMODE_FILL_WINDOWpc.FILLMODE_KEEP_ASPECT
Resolution Modes:
pc.RESOLUTION_AUTOpc.RESOLUTION_FIXED
Projection Types:
pc.PROJECTION_PERSPECTIVEpc.PROJECTION_ORTHOGRAPHIC
Light Types:
pc.LIGHTTYPE_DIRECTIONALpc.LIGHTTYPE_POINTpc.LIGHTTYPE_SPOT
Body Types:
pc.BODYTYPE_STATICpc.BODYTYPE_DYNAMICpc.BODYTYPE_KINEMATIC
Blend Types:
pc.BLEND_NONEpc.BLEND_NORMALpc.BLEND_ADDITIVEpc.BLEND_MULTIPLICATIVE
---
License
MIT - Free for commercial and personal use.
PlayCanvas Editor Workflow Guide
Complete guide for working with the PlayCanvas Editor and integrating with the engine.
---
Table of Contents
1. Editor vs Engine-Only 2. Project Setup 3. Asset Pipeline 4. Scene Management 5. Script Workflow 6. Publishing & Deployment 7. Editor-Code Integration
---
Editor vs Engine-Only
PlayCanvas Editor (Online)
Pros:
- Visual scene editing
- Real-time collaboration
- Built-in asset pipeline
- Instant preview
- Version control
- No build step required
Cons:
- Requires online access
- Less control over build process
- Tied to PlayCanvas platform
Best for: Game projects, team collaboration, rapid prototyping
---
Engine-Only (Code-First)
Pros:
- Full control over code
- Works offline
- Custom build pipeline
- Version control with Git
- No platform lock-in
Cons:
- Manual scene setup
- No visual editor
- Manual asset loading
- More code required
Best for: Web apps, embedded 3D, custom workflows
---
Project Setup
Creating Editor Project
1. Sign up at https://playcanvas.com 2. Create New Project
- Choose template (Blank, First Person, etc.)
- Set project name
3. Open Editor
- Launch visual editor
- Scene hierarchy on left
- Viewport in center
- Inspector on right
---
Editor Interface Overview
┌─────────────────────────────────────────────────────────┐
│ File Edit View Develop Tools Help [Play] [Share]│
├───────┬───────────────────────────────┬─────────────────┤
│ │ │ │
│ Scene │ Viewport │ Inspector │
│ Tree │ │ │
│ │ │ Properties │
│ ├─Camera│ [3D View] │ for selected │
│ ├─Light │ │ entity │
│ └─Cube │ │ │
│ │ │ │
├───────┴───────────────────────────────┴─────────────────┤
│ Assets Panel │
│ [Models] [Materials] [Textures] [Scripts] [Scenes] │
└─────────────────────────────────────────────────────────┘---
Project Structure
Editor Project:
MyProject/
├── assets/
│ ├── models/
│ ├── textures/
│ ├── materials/
│ ├── scripts/
│ └── scenes/
├── scenes/
│ └── Game.json
└── config.jsonExported Project:
build/
├── index.html
├── __game-scripts.js
├── __loading.js
├── __start__.js
├── config.json
├── files/
│ └── assets/
└── styles.css---
Asset Pipeline
Uploading Assets
3D Models:
- Supported:
.glb,.gltf,.fbx,.obj - Drag & drop into Assets panel
- Auto-generates material instances
Textures:
- Supported:
.jpg,.png,.dds,.ktx2,.basis - Automatically creates texture asset
- Compression options available
Audio:
- Supported:
.mp3,.ogg,.wav - 3D positional or 2D audio
Scripts:
- Create new script in Assets panel
- Opens code editor
- Hot-reload on save
---
Asset Import Settings
Model Import:
// In Editor:
// 1. Select model asset
// 2. Inspector shows import options:
// - Preserve mapping (UV mapping)
// - Create materials
// - Create animations
// - Override axisTexture Import:
// Compression settings:
// - None (original)
// - DXT (Desktop)
// - PVR (iOS)
// - ETC (Android)
// - ASTC (Modern mobile)
// - Basis (Universal)---
Creating Materials
In Editor: 1. Assets Panel → Right-click → New Material 2. Select material 3. Inspector shows material properties:
- Diffuse color
- Diffuse map
- Metalness/Gloss
- Normal map
- Emissive
- Ambient occlusion
Material Graph (Advanced):
- Visual node-based material editor
- Custom shader creation
- Real-time preview
---
Scene Management
Scene Hierarchy
Creating Entities:
Right-click in Hierarchy → Add Entity
Options:
- Empty entity
- Box, Sphere, Cylinder, etc.
- Camera
- Light
- Group (empty parent)Organizing:
- Drag entities to reparent
- Multi-select with Ctrl/Cmd
- Group related entities
- Use naming conventions:
Player
├── Model
├── Camera
└── Collider
Enemies
├── Enemy_01
├── Enemy_02
└── Enemy_03---
Components in Editor
Adding Components: 1. Select entity 2. Inspector → Add Component 3. Configure properties
Component Settings:
- Editable in Inspector
- Color pickers for colors
- Asset pickers for references
- Vector editors for positions
Example - Camera Component:
Camera Component
├── Clear Color: [Color Picker]
├── FOV: [Slider] 45°
├── Near Clip: [Input] 0.1
├── Far Clip: [Input] 1000
├── Projection: [Dropdown] Perspective
└── Priority: [Input] 0---
Tags and Layers
Tags:
- Add tags to entities for searching
- Use in scripts:
app.root.findByTag('enemy')
In Editor: 1. Select entity 2. Inspector → Tags 3. Add/remove tags
Layers:
- Control rendering order
- Separate UI from world
- Custom post-processing per layer
Default Layers:
- World (3D objects)
- UI (2D interface)
- Skybox
- Immediate (debug drawing)
---
Script Workflow
Creating Scripts in Editor
New Script: 1. Assets Panel → New Script 2. Name your script (e.g., playerController) 3. Opens code editor
Script Template:
var PlayerController = pc.createScript('playerController');
// Attributes (editable in Editor)
PlayerController.attributes.add('speed', {
type: 'number',
default: 10,
title: 'Movement Speed'
});
PlayerController.attributes.add('jumpForce', {
type: 'number',
default: 5
});
// Initialize
PlayerController.prototype.initialize = function() {
this.velocity = new pc.Vec3();
};
// Update every frame
PlayerController.prototype.update = function(dt) {
var forward = this.entity.forward;
var right = this.entity.right;
// Movement
if (this.app.keyboard.isPressed(pc.KEY_W)) {
this.entity.translate(forward.mulScalar(this.speed * dt));
}
if (this.app.keyboard.isPressed(pc.KEY_SPACE)) {
this.entity.rigidbody.applyImpulse(0, this.jumpForce, 0);
}
};---
Attaching Scripts
In Editor: 1. Select entity 2. Add Component → Script 3. Add Script → Select your script 4. Configure attributes in Inspector
Script Attributes in Inspector:
Script Component
└── playerController
├── Speed: [10]
├── Jump Force: [5]
└── [Add Script]---
Script Communication
Between Scripts:
// In one script
this.entity.script.otherScript.doSomething();
// Fire events
this.app.fire('player:died', playerEntity);
// Listen to events
this.app.on('enemy:spawned', function(enemy) {
console.log('Enemy spawned:', enemy.name);
});---
Debugging Scripts
Console Logging:
console.log('Player position:', this.entity.getPosition());
console.warn('Low health!');
console.error('Failed to load asset');Launch Tab:
- Editor → Launch → Opens game in new tab
- F12 for DevTools
- Console shows logs
- Edit scripts in Editor, auto-reloads
---
Publishing & Deployment
Publishing from Editor
Steps: 1. Build
- Settings → Publishing
- Configure build settings
- Click "Publish"
2. Download Build
- Download ZIP
- Extract files
- Upload to web server
Build Settings:
{
"name": "My Game",
"version": "1.0.0",
"scenes": [
{ "url": "game.json" }
],
"use_device_pixel_ratio": true,
"resolution_mode": "AUTO",
"fill_mode": "FILL_WINDOW",
"width": 1280,
"height": 720
}---
Hosting Options
PlayCanvas Hosting:
- Click "Publish to PlayCanvas"
- Get shareable URL
- Free with watermark
- Premium for custom domain
Self-Hosting: 1. Download build 2. Upload to:
- Netlify: Drag & drop
- Vercel: Connect GitHub
- GitHub Pages: Push to gh-pages branch
- AWS S3: Static website hosting
- Any web server: Just upload files
CORS Considerations:
// If loading assets from different domain
// Server must send CORS headers:
Access-Control-Allow-Origin: *---
Optimization for Production
Before Publishing:
1. Texture Compression
- Use Basis for universal compression
- Or platform-specific (DXT/PVR/ETC)
2. Script Concatenation
- Editor automatically concatenates scripts
- Minifies in production builds
3. Asset Loading
- Preload critical assets
- Lazy load optional content
4. Scene Settings
- Disable debug rendering
- Set appropriate quality settings
---
Editor-Code Integration
Exporting Editor Scenes
Option 1: REST API
// Download scene JSON via PlayCanvas API
fetch('https://playcanvas.com/api/projects/{id}/scenes/{sceneId}', {
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
}
})
.then(res => res.json())
.then(sceneData => {
console.log('Scene data:', sceneData);
});Option 2: Download Build
- Editor → Settings → Publishing → Download
- Extract ZIP
- Use
config.jsonand scene files in engine code
---
Loading Editor Scenes in Code
import * as pc from 'playcanvas';
const app = new pc.Application(canvas);
// Load scene from Editor export
fetch('config.json')
.then(res => res.json())
.then(config => {
// Load scene hierarchy
const sceneUrl = config.scenes[0].url;
app.scenes.loadSceneHierarchy(sceneUrl, (err, parent) => {
if (err) {
console.error('Failed to load scene:', err);
return;
}
console.log('Scene loaded');
// Find entities
const player = app.root.findByName('Player');
const camera = app.root.findByName('Camera');
// Start application
app.start();
});
// Load scene settings
app.scenes.loadSceneSettings(config.scenes[0].url, (err) => {
if (err) console.error('Failed to load settings:', err);
});
});---
Hybrid Workflow
Editor for Scenes, Code for Logic:
1. Design in Editor
- Create scene layout
- Configure entities
- Set up components
2. Export Scene
- Download build
- Get scene JSON files
3. Code Logic
- Load scenes in code
- Add custom game logic
- Extend with JavaScript
Example:
// Load Editor scene
app.scenes.loadSceneHierarchy('scene.json', (err, root) => {
if (err) return;
// Find Editor entities
const player = root.findByName('Player');
const enemies = root.findByTag('enemy');
// Add code-based logic
const gameManager = new pc.Entity('GameManager');
gameManager.addComponent('script');
gameManager.script.create('gameManager');
app.root.addChild(gameManager);
// Start game
app.start();
app.fire('game:start');
});---
Accessing Editor Attributes in Code
In Editor Script:
var MyScript = pc.createScript('myScript');
MyScript.attributes.add('target', {
type: 'entity',
title: 'Target Entity'
});
MyScript.attributes.add('speed', {
type: 'number',
default: 10
});Accessing from Other Scripts:
// Get script instance
const myScript = entity.script.myScript;
// Read attributes
console.log('Target:', myScript.target);
console.log('Speed:', myScript.speed);
// Modify at runtime
myScript.speed = 20;---
Version Control
Editor Projects
Built-in Version Control:
- Editor → Version Control
- Checkpoint system
- Branch management
- Conflict resolution
Checkpoints:
- Create checkpoint before major changes
- Name checkpoints descriptively
- Restore previous versions
---
Engine-Only Projects
Git Workflow:
git init
git add .
git commit -m "Initial PlayCanvas project"
# .gitignore
node_modules/
build/
*.log
.DS_StoreStructure:
project/
├── src/
│ ├── index.html
│ ├── main.js
│ └── scripts/
├── assets/
│ ├── models/
│ └── textures/
├── package.json
└── README.md---
Collaboration
Editor Collaboration
Real-time Editing:
- Multiple users in same project
- See others' cursors
- Live updates
- Chat built-in
Permissions:
- Owner (full access)
- Admin (edit + manage)
- Write (edit only)
- Read (view only)
---
Team Workflow
Best Practices:
1. Scene Ownership
- One person per scene at a time
- Use checkpoints before switching
2. Asset Organization
- Consistent naming:
character_player_diffuse.png - Folder structure:
assets/characters/player/
3. Script Conventions
- camelCase for functions
- PascalCase for classes
- Prefix custom scripts:
game_playerController.js
4. Communication
- Use Editor chat
- Document major changes
- Code reviews for critical scripts
---
Debugging in Editor
Launch & Debug
Launch Tab:
- Editor → Launch
- Opens game in new tab with DevTools
Debug Options:
- Profiler: Performance metrics
- Inspector: Scene graph at runtime
- Console: Logs and errors
Profiler:
Launch → Profiler
Shows:
- FPS
- Draw calls
- Triangle count
- Texture memory
- Script execution time---
Editor Console
Accessing Engine in Console:
// In Launch tab console:
pc.app // Application instance
pc.app.root // Root entity
pc.app.systems // Component systems
// Find entities
pc.app.root.findByName('Player')
pc.app.root.findByTag('enemy')
// Inspect entities
const player = pc.app.root.findByName('Player');
console.log('Position:', player.getPosition());
console.log('Components:', player.c); // All components---
Tips & Best Practices
Performance
In Editor:
- Use LOD for complex models
- Enable frustum culling
- Batch static objects
- Compress textures
Scene Organization:
- Group related entities
- Disable entities instead of destroying
- Use object pooling for bullets/particles
---
Asset Management
Naming Conventions:
Models: char_player.glb
Textures: char_player_diffuse.png
Materials: mat_player_body
Scripts: player_controller.js
Scenes: level_01.jsonFolder Structure:
assets/
├── characters/
│ ├── player/
│ └── enemies/
├── environment/
│ ├── props/
│ └── terrain/
├── fx/
│ ├── particles/
│ └── sounds/
└── ui/---
Editor Shortcuts
Navigation:
F: Frame selected entityW/A/S/D: Move viewportQ/E: Up/downRight-click + drag: Rotate viewMiddle-click + drag: Pan viewScroll: Zoom
Editing:
Ctrl/Cmd + D: Duplicate entityDelete: Delete entityCtrl/Cmd + Z: UndoCtrl/Cmd + Shift + Z: RedoCtrl/Cmd + G: Group entities
Tools:
1: Select mode2: Translate mode3: Rotate mode4: Scale modeSpace: Toggle between modes
---
Resources
- Editor: https://playcanvas.com
- Developer Docs: https://developer.playcanvas.com
- API Reference: https://api.playcanvas.com
- Tutorials: https://developer.playcanvas.com/tutorials/
- Forum: https://forum.playcanvas.com
- Examples: https://playcanvas.github.io
---
Quick Reference
Editor Workflow
1. Create project
2. Import assets
3. Build scene hierarchy
4. Add components
5. Attach scripts
6. Configure settings
7. Test in Launch tab
8. Publish/DownloadScript Workflow
1. Assets → New Script
2. Write code in editor
3. Attach to entity
4. Configure attributes
5. Test in Launch tab
6. Debug in console
7. IteratePublishing Workflow
1. Settings → Publishing
2. Configure build settings
3. Click Publish
4. Download build
5. Upload to hosting
6. Test live site---
This guide covers the complete PlayCanvas Editor workflow from project creation to deployment. Use the Editor for visual scene design and the Engine API for custom code logic.
Related skills
FAQ
What does playcanvas-engine do?
Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interactive 3D applications, or performance-critical web experience
When should I use playcanvas-engine?
Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games, interactive 3D applications, or performance-critical web experience
What are common prerequisites?
--- name: playcanvas-engine description: Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration.
Is Playcanvas Engine safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.