关于 playcanvas-engine
playcanvas-engine 是一项使用 PlayCanvas 引擎开发浏览器 3D 应用和游戏的技能,面向开发者。它涵盖实体-组件架构、编辑器工作流、渲染、资源、输入、动画与性能优化。
主要功能
- 01实体-组件场景架构
- 02WebGL 与 WebGPU 引擎设置
- 03GLTF 资源加载与管理
- 04物理、输入和动画模式
- 05PlayCanvas Editor 项目集成
- 06渲染与内存优化
适用场景
- 01搭建浏览器 3D 场景
- 02加载并显示 GLTF 模型
- 03使用实体和组件构建游戏逻辑
- 04优化 3D 应用性能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ freshtechbro/ claudedesignskills --skill playcanvas-engine安装后在 Claude Code 中输入/playcanvas-engine确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/examples/README.mdassets/starter_playcanvas/index.htmlREADME.mdstyles.cssassets/starter_playcanvas/scripts/app.jscamera-controller.jsinput-manager.jsreferences/api_reference.mdeditor_workflow.mdoptimization_guide.mdscripts/component_builder.pyproject_generator.py详细介绍
PlayCanvas 引擎技能
轻量级 WebGL/WebGPU 游戏引擎,采用实体-组件架构,集成可视化编辑器,并注重性能。
何时使用此技能
遇到以下情况时,触发此技能:
- “PlayCanvas 引擎”
- “WebGL 游戏引擎”
- “实体组件系统”
- “PlayCanvas 应用”
- “3D 浏览器游戏”
- “在线 3D 编辑器”
- “轻量级 3D 引擎”
- 需要以编辑器为先的工作流
与以下方案比较:
- Three.js:层级更低、灵活性更高,但需要更多设置
- Babylon.js:功能丰富,但体量较大;虽有编辑器,但成熟度较低
- A-Frame:专注于 VR,采用声明式 HTML 方式
- 以下场景使用 PlayCanvas:游戏项目、以编辑器为先的工作流、对性能要求高的应用
核心概念
1. 应用
PlayCanvas 应用根节点负责管理渲染循环。
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. 实体-组件系统
PlayCanvas 采用 ECS 架构:实体包含组件。
// 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. 更新循环
应用会在更新循环期间触发事件。
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. 组件
核心组件可扩展实体的功能:
模型组件:
entity.addComponent('model', {
type: 'box', // 'box', 'sphere', 'cylinder', 'cone', 'capsule', 'asset'
material: material,
castShadows: true,
receiveShadows: true
});
摄像机组件:
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
});
光源组件:
entity.addComponent('light', {
type: pc.LIGHTTYPE_DIRECTIONAL, // DIRECTIONAL, POINT, SPOT
color: new pc.Color(1, 1, 1),
intensity: 1,
castShadows: true,
shadowDistance: 50
});
刚体组件(需要物理系统):
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)
});
常见模式
模式 1:基本场景设置
创建包含摄像机、光源和模型的完整场景。
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();
模式 2:加载 GLTF 模型
通过资源管理加载外部 3D 模型。
// 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);
带错误处理:
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);
模式 3:材质与纹理
使用 PBR 工作流创建自定义材质。
// 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);
});
模式 4:物理集成
使用 Ammo.js 进行物理模拟。
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/[email protected]/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();
});
};
模式 5:自定义脚本
创建可复用的脚本组件。
// 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);
脚本生命周期方法:
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
};
模式 6:输入处理
处理键盘、鼠标和触摸输入。
// 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);
}
});
模式 7:动画
播放骨骼动画和补间动画。
骨骼动画:
// 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);
属性补间:
// 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();
集成模式
集成 1:React 集成
将 PlayCanvas 封装在 React 组件中。
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;
集成 2:编辑器导出
使用 PlayCanvas Editor 项目。
// 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();
});
});
性能优化
1. 对象池
重复使用实体,而不是创建和销毁实体。
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(细节层次)
减少远处物体的几何细节。
// 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. 批处理
合并静态网格以减少绘制调用。
// Enable static batching for entity
entity.model.batchGroupId = 1;
// Batch all entities with same group ID
app.batcher.generate([entity1, entity2, entity3]);
4. 纹理压缩
使用压缩纹理格式。
// 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
});
常见问题
问题 1:未启动应用
问题:场景已渲染,但没有任何反应。
// ❌ 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!
问题 2:在更新期间修改实体
问题:迭代时修改场景图。
// ❌ 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;
});
问题 3:资源导致内存泄漏
问题:未清理已加载的资源。
// ❌ 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();
问题 4:变换层级不正确
问题:变换未能正确传递。
// ❌ 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
问题 5:物理系统未初始化
问题:物理组件无法工作。
// ❌ 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/[email protected]/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' });
});
};
问题 6:画布尺寸问题
问题:画布未填满容器,或无法响应尺寸调整。
// ❌ 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());
资源
- 官方 API:https://api.playcanvas.com/
- 开发者文档:https://developer.playcanvas.com/
- 示例:https://playcanvas.github.io/
- 编辑器:https://playcanvas.com/
- GitHub:https://github.com/playcanvas/engine
- 论坛:https://forum.playcanvas.com/
快速参考
应用设置
const app = new pc.Application(canvas);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
app.start();
创建实体
const entity = new pc.Entity('name');
entity.addComponent('model', { type: 'box' });
entity.setPosition(x, y, z);
app.root.addChild(entity);
更新循环
app.on('update', (dt) => {
// Logic here
});
加载资源
const asset = new pc.Asset('name', 'type', { url: '/path' });
app.assets.add(asset);
asset.ready(() => { /* use asset */ });
app.assets.load(asset);
相关技能:如需更底层的 WebGL 控制,请参考 threejs-webgl。如需 React 集成模式,请参阅 react-three-fiber。如需用于物理密集型模拟的引擎,请参考 babylonjs-engine。