playcanvas-engine

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PlayCanvas Engine Skill

PlayCanvas 引擎技能指南

Lightweight WebGL/WebGPU game engine with entity-component architecture, visual editor integration, and performance-focused design.
这是一款轻量级WebGL/WebGPU游戏引擎,采用实体组件架构,集成了可视化编辑器,且以性能为核心进行设计。

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

当你遇到以下场景时,可使用本技能:
  • "PlayCanvas engine"
  • "WebGL game engine"
  • "entity component system"
  • "PlayCanvas application"
  • "3D browser games"
  • "online 3D editor"
  • "lightweight 3D engine"
  • 需要编辑器优先的工作流
对比竞品:
  • Three.js:底层级、灵活性更高,但需要更多配置工作
  • Babylon.js:功能丰富但体积更大,虽有编辑器但成熟度不足
  • A-Frame:专注VR领域,采用声明式HTML开发方式
  • 选择PlayCanvas的场景:游戏项目、编辑器优先工作流、对性能要求高的应用

Core Concepts

核心概念

1. Application

1. 应用程序

The root PlayCanvas application manages the rendering loop.
javascript
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();

PlayCanvas的根应用程序负责管理渲染循环。
javascript
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

2. 实体组件系统

PlayCanvas uses ECS architecture: Entities contain Components.
javascript
// 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);

PlayCanvas采用ECS架构:实体包含组件。
javascript
// 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

3. 更新循环

The application fires events during the update loop.
javascript
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
});

应用程序会在更新循环中触发事件。
javascript
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

4. 组件

Core components extend entity functionality:
Model Component:
javascript
entity.addComponent('model', {
  type: 'box',           // 'box', 'sphere', 'cylinder', 'cone', 'capsule', 'asset'
  material: material,
  castShadows: true,
  receiveShadows: true
});
Camera Component:
javascript
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:
javascript
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):
javascript
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)
});

核心组件可扩展实体的功能:
模型组件:
javascript
entity.addComponent('model', {
  type: 'box',           // 'box', 'sphere', 'cylinder', 'cone', 'capsule', 'asset'
  material: material,
  castShadows: true,
  receiveShadows: true
});
相机组件:
javascript
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
});
灯光组件:
javascript
entity.addComponent('light', {
  type: pc.LIGHTTYPE_DIRECTIONAL,  // DIRECTIONAL, POINT, SPOT
  color: new pc.Color(1, 1, 1),
  intensity: 1,
  castShadows: true,
  shadowDistance: 50
});
刚体组件(需要物理引擎支持):
javascript
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

模式1:基础场景搭建

Create a complete scene with camera, light, and models.
javascript
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();

创建包含相机、灯光和模型的完整场景。
javascript
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

模式2:加载GLTF模型

Load external 3D models with asset management.
javascript
// 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:
javascript
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);

通过资源管理器加载外部3D模型。
javascript
// 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);
带错误处理的版本:
javascript
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

模式3:材质与纹理

Create custom materials with PBR workflow.
javascript
// 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);
});

使用PBR工作流创建自定义材质。
javascript
// 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

模式4:物理引擎集成

Use Ammo.js for physics simulation.
javascript
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();
  });
};

使用Ammo.js进行物理模拟。
javascript
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

模式5:自定义脚本

Create reusable script components.
javascript
// 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:
javascript
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
};

创建可复用的脚本组件。
javascript
// 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);
脚本生命周期方法:
javascript
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

模式6:输入处理

Handle keyboard, mouse, and touch input.
javascript
// 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);
  }
});

处理键盘、鼠标与触摸输入。
javascript
// 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

模式7:动画系统

Play skeletal animations and tweens.
Skeletal animation:
javascript
// 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:
javascript
// 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();

播放骨骼动画与补间动画。
骨骼动画:
javascript
// 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);
属性补间动画:
javascript
// 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

集成1:React 集成

Wrap PlayCanvas in React components.
jsx
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;

在React组件中封装PlayCanvas。
jsx
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

集成2:编辑器导出

Work with PlayCanvas Editor projects.
javascript
// 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();
    });
  });

与PlayCanvas编辑器项目配合使用。
javascript
// 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

1. 对象池技术

Reuse entities instead of creating/destroying.
javascript
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);

复用实体而非频繁创建/销毁。
javascript
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)

2. 细节层次(LOD)

Reduce geometry for distant objects.
javascript
// 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;
  });
});

对远距离对象简化几何体。
javascript
// 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

3. 批处理

Combine static meshes to reduce draw calls.
javascript
// Enable static batching for entity
entity.model.batchGroupId = 1;

// Batch all entities with same group ID
app.batcher.generate([entity1, entity2, entity3]);

合并静态网格以减少绘制调用。
javascript
// 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

4. 纹理压缩

Use compressed texture formats.
javascript
// 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
});

使用压缩纹理格式。
javascript
// 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

陷阱1:未启动应用程序

Problem: Scene renders but nothing happens.
javascript
// ❌ 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!

问题:场景可渲染但无任何交互或动画。
javascript
// ❌ 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

陷阱2:在更新循环中修改实体

Problem: Modifying scene graph during iteration.
javascript
// ❌ 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;
});

问题:遍历场景图时修改其结构。
javascript
// ❌ 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

陷阱3:资源未清理导致内存泄漏

Problem: Not cleaning up loaded assets.
javascript
// ❌ 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();

问题:已加载的资源未被清理,长期占用内存。
javascript
// ❌ 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

陷阱4:变换层级错误

Problem: Transforms not propagating correctly.
javascript
// ❌ 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

问题:变换属性未正确传递给子实体。
javascript
// ❌ 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

陷阱5:物理引擎未初始化

Problem: Physics components don't work.
javascript
// ❌ 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' });
  });
};

问题:物理组件无法正常工作。
javascript
// ❌ 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

陷阱6:画布尺寸问题

Problem: Canvas doesn't fill container or respond to resize.
javascript
// ❌ 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());

问题:画布无法填充容器或不响应窗口大小变化。
javascript
// ❌ 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

应用程序初始化

javascript
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
app.start();
javascript
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
app.start();

Entity Creation

实体创建

javascript
const entity = new pc.Entity('name');
entity.addComponent('model', { type: 'box' });
entity.setPosition(x, y, z);
app.root.addChild(entity);
javascript
const entity = new pc.Entity('name');
entity.addComponent('model', { type: 'box' });
entity.setPosition(x, y, z);
app.root.addChild(entity);

Update Loop

更新循环

javascript
app.on('update', (dt) => {
  // Logic here
});
javascript
app.on('update', (dt) => {
  // 业务逻辑写在这里
});

Loading Assets

资源加载

javascript
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.
javascript
const asset = new pc.Asset('name', 'type', { url: '/path' });
app.assets.add(asset);
asset.ready(() => { /* 使用资源 */ });
app.assets.load(asset);

相关技能:如需更底层的WebGL控制,可参考threejs-webgl技能;如需React集成模式,可查看react-three-fiber;如需物理模拟密集型场景,可参考babylonjs-engine技能。