playcanvas-engine

Lightweight WebGL/WebGPU game engine with entity-component architecture and visual editor integration. Use this skill when building browser-based games,…

INSTALLATION
npx skills add https://github.com/freshtechbro/claudedesignskills --skill playcanvas-engine
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

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.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card