在前面的章节中,我们学习了Canvas的基础知识、绘图API、动画技术和交互事件处理等内容。随着Canvas应用的复杂度增加,性能问题变得越来越重要。本章将介绍Canvas性能优化的关键技术和最佳实践,帮助你创建高效、流畅的Canvas应用。
10.1 性能分析与测量
在进行优化之前,我们需要了解应用的性能瓶颈在哪里。这一节将介绍如何测量和分析Canvas应用的性能。
10.1.1 性能指标
以下是评估Canvas应用性能的关键指标:
- 帧率(FPS):每秒渲染的帧数,通常60FPS被认为是流畅的。
- CPU使用率:Canvas操作主要在CPU上执行,高CPU使用率可能导致应用卡顿。
- 内存使用:过高的内存使用可能导致垃圾回收频繁,影响性能。
- 渲染时间:每帧的渲染时间,理想情况下应小于16.67ms(60FPS)。
10.1.2 性能测量工具
浏览器开发者工具
现代浏览器提供了强大的性能分析工具:
// 使用Performance API测量代码执行时间
const startTime = performance.now();
// 执行需要测量的代码
drawComplexScene();
const endTime = performance.now();
console.log(`绘制场景耗时: ${endTime - startTime} ms`);
自定义FPS计数器
class FPSCounter {
constructor() {
this.fps = 0;
this.frames = 0;
this.lastTime = performance.now();
}
update() {
this.frames++;
const now = performance.now();
const delta = now - this.lastTime;
if (delta >= 1000) {
this.fps = Math.round((this.frames * 1000) / delta);
this.lastTime = now;
this.frames = 0;
}
return this.fps;
}
draw(ctx, x, y) {
const fps = this.update();
ctx.font = '16px Arial';
ctx.fillStyle = fps > 30 ? 'green' : (fps > 15 ? 'yellow' : 'red');
ctx.fillText(`FPS: ${fps}`, x, y);
}
}
// 使用方法
const fpsCounter = new FPSCounter();
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制场景
drawScene();
// 显示FPS
fpsCounter.draw(ctx, 10, 20);
}
animate();
10.1.3 性能分析示例
以下是一个完整的性能分析示例,包括测量渲染时间和内存使用:
class PerformanceMonitor {
constructor() {
this.fpsCounter = new FPSCounter();
this.renderTimes = [];
this.memoryUsage = [];
this.maxSamples = 60; // 保存最近60帧的数据
}
startFrame() {
this.frameStartTime = performance.now();
}
endFrame(ctx, x, y) {
const renderTime = performance.now() - this.frameStartTime;
this.renderTimes.push(renderTime);
// 如果浏览器支持内存信息API
if (performance.memory) {
this.memoryUsage.push(performance.memory.usedJSHeapSize / (1024 * 1024));
}
// 保持数组长度
if (this.renderTimes.length > this.maxSamples) {
this.renderTimes.shift();
}
if (this.memoryUsage.length > this.maxSamples) {
this.memoryUsage.shift();
}
// 计算平均渲染时间
const avgRenderTime = this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length;
// 显示性能信息
ctx.font = '14px Arial';
ctx.fillStyle = 'black';
ctx.fillText(`FPS: ${this.fpsCounter.update()}`, x, y);
ctx.fillText(`平均渲染时间: ${avgRenderTime.toFixed(2)} ms`, x, y + 20);
if (performance.memory) {
const avgMemory = this.memoryUsage.reduce((a, b) => a + b, 0) / this.memoryUsage.length;
ctx.fillText(`内存使用: ${avgMemory.toFixed(2)} MB`, x, y + 40);
}
}
// 绘制性能图表
drawChart(ctx, x, y, width, height) {
// 绘制渲染时间图表
ctx.strokeStyle = 'blue';
ctx.beginPath();
const maxTime = Math.max(...this.renderTimes, 16.67) * 1.2; // 最大值,至少包含16.67ms
for (let i = 0; i < this.renderTimes.length; i++) {
const xPos = x + (i / this.maxSamples) * width;
const yPos = y + height - (this.renderTimes[i] / maxTime) * height;
if (i === 0) {
ctx.moveTo(xPos, yPos);
} else {
ctx.lineTo(xPos, yPos);
}
}
ctx.stroke();
// 绘制16.67ms参考线(60FPS)
const refLineY = y + height - (16.67 / maxTime) * height;
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(x, refLineY);
ctx.lineTo(x + width, refLineY);
ctx.stroke();
ctx.fillStyle = 'red';
ctx.fillText('16.67ms (60FPS)', x + width + 5, refLineY + 5);
}
}
// 使用方法
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const perfMonitor = new PerformanceMonitor();
function animate() {
requestAnimationFrame(animate);
perfMonitor.startFrame();
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制场景
drawScene();
// 结束帧并显示性能信息
perfMonitor.endFrame(ctx, 10, 20);
// 绘制性能图表
perfMonitor.drawChart(ctx, 10, 70, 300, 100);
}
animate();
10.2 渲染优化策略
10.2.1 分层渲染与缓存
一个重要的优化策略是使用多个Canvas层和离屏渲染来减少重绘开销。
多层Canvas
<div class="canvas-container" style="position: relative;">
<canvas id="background" style="position: absolute; left: 0; top: 0;"></canvas>
<canvas id="main" style="position: absolute; left: 0; top: 0;"></canvas>
<canvas id="ui" style="position: absolute; left: 0; top: 0;"></canvas>
</div>
const backgroundCanvas = document.getElementById('background');
const mainCanvas = document.getElementById('main');
const uiCanvas = document.getElementById('ui');
const bgCtx = backgroundCanvas.getContext('2d');
const mainCtx = mainCanvas.getContext('2d');
const uiCtx = uiCanvas.getContext('2d');
// 设置所有Canvas的尺寸
function resizeCanvases() {
const width = window.innerWidth;
const height = window.innerHeight;
[backgroundCanvas, mainCanvas, uiCanvas].forEach(canvas => {
canvas.width = width;
canvas.height = height;
});
// 重新绘制背景(只需在尺寸变化或背景变化时绘制)
drawBackground();
}
// 绘制静态背景
function drawBackground() {
// 绘制不经常变化的背景元素
bgCtx.fillStyle = 'skyblue';
bgCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height);
// 绘制山脉、树木等静态元素
drawMountains(bgCtx);
drawTrees(bgCtx);
}
// 主循环只更新需要动画的元素
function animate() {
requestAnimationFrame(animate);
// 清除动态内容的Canvas
mainCtx.clearRect(0, 0, mainCanvas.width, mainCanvas.height);
// 绘制动态游戏元素
drawGameObjects(mainCtx);
// 更新UI(如果需要)
updateUI(uiCtx);
}
// 初始化
window.addEventListener('resize', resizeCanvases);
resizeCanvases();
animate();
离屏渲染与缓存
对于不经常变化的复杂图形,可以使用离屏Canvas进行预渲染:
class CachedDrawing {
constructor(drawFunction, width, height) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = width;
this.cacheCanvas.height = height;
this.cacheCtx = this.cacheCanvas.getContext('2d');
// 执行绘制函数,将结果缓存到离屏Canvas
drawFunction(this.cacheCtx);
}
// 在目标Canvas上绘制缓存的图像
draw(ctx, x, y, width, height) {
width = width || this.cacheCanvas.width;
height = height || this.cacheCanvas.height;
ctx.drawImage(this.cacheCanvas, x, y, width, height);
}
// 更新缓存(当需要改变时)
update(drawFunction) {
this.cacheCtx.clearRect(0, 0, this.cacheCanvas.width, this.cacheCanvas.height);
drawFunction(this.cacheCtx);
}
}
// 使用示例:缓存一个复杂的树
const treeCache = new CachedDrawing((ctx) => {
// 绘制复杂的树
drawDetailedTree(ctx, 0, 0, 100, 200);
}, 100, 200);
// 在主循环中使用缓存的树
function drawScene() {
// 绘制50棵相同的树,但只需渲染一次
for (let i = 0; i < 50; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
treeCache.draw(ctx, x, y);
}
}
10.2.2 批量处理与减少状态更改
每次更改Canvas状态(如填充颜色、线宽等)都会产生开销。通过批量处理相似的绘制操作,可以减少状态更改:
// 低效方式:频繁切换状态
function drawCirclesInefficient(ctx, circles) {
circles.forEach(circle => {
ctx.beginPath();
ctx.fillStyle = circle.color;
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
ctx.fill();
});
}
// 优化方式:按颜色分组批量处理
function drawCirclesOptimized(ctx, circles) {
// 按颜色分组
const circlesByColor = {};
circles.forEach(circle => {
if (!circlesByColor[circle.color]) {
circlesByColor[circle.color] = [];
}
circlesByColor[circle.color].push(circle);
});
// 按颜色批量绘制
Object.keys(circlesByColor).forEach(color => {
ctx.fillStyle = color;
ctx.beginPath();
circlesByColor[color].forEach(circle => {
ctx.moveTo(circle.x + circle.radius, circle.y);
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
});
ctx.fill();
});
}
10.2.3 使用适当的绘图API
选择合适的绘图API可以显著提高性能:
// 绘制大量矩形的性能比较
function drawRectanglesComparison() {
const count = 1000;
const rectangles = [];
// 生成随机矩形
for (let i = 0; i < count; i++) {
rectangles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
width: 10 + Math.random() * 40,
height: 10 + Math.random() * 40,
color: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`
});
}
// 方法1: fillRect API
function drawWithFillRect() {
rectangles.forEach(rect => {
ctx.fillStyle = rect.color;
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
});
}
// 方法2: rect + fill API
function drawWithRectAndFill() {
rectangles.forEach(rect => {
ctx.fillStyle = rect.color;
ctx.beginPath();
ctx.rect(rect.x, rect.y, rect.width, rect.height);
ctx.fill();
});
}
// 方法3: 路径API
function drawWithPath() {
rectangles.forEach(rect => {
ctx.fillStyle = rect.color;
ctx.beginPath();
ctx.moveTo(rect.x, rect.y);
ctx.lineTo(rect.x + rect.width, rect.y);
ctx.lineTo(rect.x + rect.width, rect.y + rect.height);
ctx.lineTo(rect.x, rect.y + rect.height);
ctx.closePath();
ctx.fill();
});
}
// 方法4: 批量处理(按颜色分组)
function drawWithBatching() {
const rectsByColor = {};
rectangles.forEach(rect => {
if (!rectsByColor[rect.color]) {
rectsByColor[rect.color] = [];
}
rectsByColor[rect.color].push(rect);
});
Object.keys(rectsByColor).forEach(color => {
ctx.fillStyle = color;
rectsByColor[color].forEach(rect => {
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
});
});
}
// 性能测试
console.time('fillRect');
drawWithFillRect();
console.timeEnd('fillRect');
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.time('rect+fill');
drawWithRectAndFill();
console.timeEnd('rect+fill');
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.time('path');
drawWithPath();
console.timeEnd('path');
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.time('batching');
drawWithBatching();
console.timeEnd('batching');
}
10.3 内存管理与垃圾回收优化
10.3.1 对象池模式
对象池模式可以减少垃圾回收的频率,特别是在需要频繁创建和销毁对象的场景中:
class ParticlePool {
constructor(size) {
this.pool = [];
this.activeParticles = [];
// 预创建粒子对象
for (let i = 0; i < size; i++) {
this.pool.push(this.createParticle());
}
}
createParticle() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
color: 'white',
size: 2,
life: 0,
maxLife: 60,
active: false,
reset: function() {
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.color = 'white';
this.size = 2;
this.life = 0;
this.maxLife = 60;
this.active = false;
}
};
}
// 获取一个粒子
get() {
// 如果池中有可用粒子,则使用它
if (this.pool.length > 0) {
const particle = this.pool.pop();
particle.active = true;
this.activeParticles.push(particle);
return particle;
}
// 如果没有可用粒子,创建一个新的
console.warn('Particle pool depleted, creating new particle');
const particle = this.createParticle();
particle.active = true;
this.activeParticles.push(particle);
return particle;
}
// 释放一个粒子回池
release(particle) {
const index = this.activeParticles.indexOf(particle);
if (index !== -1) {
this.activeParticles.splice(index, 1);
particle.reset();
this.pool.push(particle);
}
}
// 更新所有活动粒子
update() {
for (let i = this.activeParticles.length - 1; i >= 0; i--) {
const particle = this.activeParticles[i];
particle.life++;
// 如果粒子寿命结束,释放回池
if (particle.life >= particle.maxLife) {
this.release(particle);
continue;
}
// 更新粒子位置
particle.x += particle.vx;
particle.y += particle.vy;
}
}
// 绘制所有活动粒子
draw(ctx) {
ctx.save();
this.activeParticles.forEach(particle => {
const alpha = 1 - (particle.life / particle.maxLife);
ctx.globalAlpha = alpha;
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.restore();
}
}
// 使用示例
const particlePool = new ParticlePool(1000);
// 创建爆炸效果
function createExplosion(x, y, count) {
for (let i = 0; i < count; i++) {
const particle = particlePool.get();
particle.x = x;
particle.y = y;
particle.vx = (Math.random() - 0.5) * 5;
particle.vy = (Math.random() - 0.5) * 5;
particle.size = 1 + Math.random() * 3;
particle.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
particle.maxLife = 30 + Math.random() * 60;
}
}
// 在动画循环中
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和绘制粒子
particlePool.update();
particlePool.draw(ctx);
// 随机创建爆炸
if (Math.random() < 0.05) {
createExplosion(
Math.random() * canvas.width,
Math.random() * canvas.height,
50
);
}
}
10.3.2 避免闭包和临时对象
在动画循环中,应避免创建临时对象和闭包,以减少垃圾回收:
// 低效方式:每帧创建新对象
function updateInefficient() {
entities.forEach(entity => {
// 每次调用都创建新对象
const velocity = { x: entity.speed * Math.cos(entity.angle), y: entity.speed * Math.sin(entity.angle) };
entity.x += velocity.x;
entity.y += velocity.y;
});
}
// 优化方式:重用对象
const tempVelocity = { x: 0, y: 0 };
function updateOptimized() {
entities.forEach(entity => {
// 重用同一个对象
tempVelocity.x = entity.speed * Math.cos(entity.angle);
tempVelocity.y = entity.speed * Math.sin(entity.angle);
entity.x += tempVelocity.x;
entity.y += tempVelocity.y;
});
}
10.3.3 使用TypedArray和二进制操作
对于大量数据处理,使用TypedArray可以提高性能:
// 使用TypedArray存储粒子数据
class OptimizedParticleSystem {
constructor(maxParticles) {
this.maxParticles = maxParticles;
this.count = 0;
// 使用TypedArray存储粒子属性
this.positions = new Float32Array(maxParticles * 2); // x, y
this.velocities = new Float32Array(maxParticles * 2); // vx, vy
this.colors = new Uint32Array(maxParticles); // ARGB格式
this.sizes = new Float32Array(maxParticles);
this.lives = new Float32Array(maxParticles * 2); // currentLife, maxLife
}
addParticle(x, y, vx, vy, color, size, maxLife) {
if (this.count >= this.maxParticles) return false;
const i = this.count++;
// 设置粒子属性
this.positions[i * 2] = x;
this.positions[i * 2 + 1] = y;
this.velocities[i * 2] = vx;
this.velocities[i * 2 + 1] = vy;
this.colors[i] = color;
this.sizes[i] = size;
this.lives[i * 2] = 0; // currentLife
this.lives[i * 2 + 1] = maxLife; // maxLife
return true;
}
update() {
let aliveCount = 0;
for (let i = 0; i < this.count; i++) {
const currentLifeIdx = i * 2;
const maxLifeIdx = currentLifeIdx + 1;
// 增加生命值
this.lives[currentLifeIdx] += 1;
// 检查粒子是否存活
if (this.lives[currentLifeIdx] >= this.lives[maxLifeIdx]) {
continue;
}
// 更新位置
const posIdx = i * 2;
const velIdx = i * 2;
this.positions[posIdx] += this.velocities[velIdx];
this.positions[posIdx + 1] += this.velocities[velIdx + 1];
// 如果粒子仍然存活,将其移动到活动数组的前部
if (i !== aliveCount) {
// 交换位置
this.positions[aliveCount * 2] = this.positions[posIdx];
this.positions[aliveCount * 2 + 1] = this.positions[posIdx + 1];
// 交换速度
this.velocities[aliveCount * 2] = this.velocities[velIdx];
this.velocities[aliveCount * 2 + 1] = this.velocities[velIdx + 1];
// 交换颜色和大小
this.colors[aliveCount] = this.colors[i];
this.sizes[aliveCount] = this.sizes[i];
// 交换生命值
this.lives[aliveCount * 2] = this.lives[currentLifeIdx];
this.lives[aliveCount * 2 + 1] = this.lives[maxLifeIdx];
}
aliveCount++;
}
this.count = aliveCount;
}
draw(ctx) {
for (let i = 0; i < this.count; i++) {
const posIdx = i * 2;
const x = this.positions[posIdx];
const y = this.positions[posIdx + 1];
const size = this.sizes[i];
// 从ARGB格式解析颜色
const color = this.colors[i];
const r = (color >> 16) & 0xFF;
const g = (color >> 8) & 0xFF;
const b = color & 0xFF;
const a = ((color >> 24) & 0xFF) / 255;
// 计算透明度衰减
const lifeRatio = this.lives[i * 2] / this.lives[i * 2 + 1];
const alpha = 1 - lifeRatio;
ctx.globalAlpha = alpha * a;
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// 创建爆炸效果
createExplosion(x, y, count, minSpeed, maxSpeed) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = minSpeed + Math.random() * (maxSpeed - minSpeed);
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
// 生成随机颜色(ARGB格式)
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
const color = (255 << 24) | (r << 16) | (g << 8) | b;
const size = 1 + Math.random() * 3;
const maxLife = 30 + Math.random() * 30;
this.addParticle(x, y, vx, vy, color, size, maxLife);
}
}
}
// 使用示例
const particleSystem = new OptimizedParticleSystem(10000);
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和绘制粒子
particleSystem.update();
particleSystem.draw(ctx);
// 随机创建爆炸
if (Math.random() < 0.05) {
particleSystem.createExplosion(
Math.random() * canvas.width,
Math.random() * canvas.height,
100,
1,
5
);
}
}
10.4 视口优化与可见性检测
10.4.1 视口裁剪
只渲染视口内可见的对象可以大幅提高性能:
class Viewport {
constructor(canvas, worldWidth, worldHeight) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
// 世界尺寸
this.worldWidth = worldWidth;
this.worldHeight = worldHeight;
// 视口位置和尺寸
this.x = 0;
this.y = 0;
this.width = canvas.width;
this.height = canvas.height;
// 缩放级别
this.scale = 1;
}
// 移动视口
move(dx, dy) {
this.x = Math.max(0, Math.min(this.x + dx, this.worldWidth - this.width / this.scale));
this.y = Math.max(0, Math.min(this.y + dy, this.worldHeight - this.height / this.scale));
}
// 缩放视口
zoom(factor, centerX, centerY) {
// 保存缩放前的世界坐标
const worldX = this.screenToWorldX(centerX);
const worldY = this.screenToWorldY(centerY);
// 应用缩放
this.scale = Math.max(0.1, Math.min(5, this.scale * factor));
// 调整视口位置,使缩放中心点保持在相同的屏幕位置
this.x = worldX - centerX / this.scale;
this.y = worldY - centerY / this.scale;
// 确保视口不超出世界边界
this.x = Math.max(0, Math.min(this.x, this.worldWidth - this.width / this.scale));
this.y = Math.max(0, Math.min(this.y, this.worldHeight - this.height / this.scale));
}
// 屏幕坐标转世界坐标
screenToWorldX(screenX) {
return this.x + screenX / this.scale;
}
screenToWorldY(screenY) {
return this.y + screenY / this.scale;
}
// 世界坐标转屏幕坐标
worldToScreenX(worldX) {
return (worldX - this.x) * this.scale;
}
worldToScreenY(worldY) {
return (worldY - this.y) * this.scale;
}
// 检查对象是否在视口内
isVisible(x, y, width, height) {
return (
x + width >= this.x &&
x <= this.x + this.width / this.scale &&
y + height >= this.y &&
y <= this.y + this.height / this.scale
);
}
// 开始绘制(应用视口变换)
begin() {
this.ctx.save();
this.ctx.scale(this.scale, this.scale);
this.ctx.translate(-this.x, -this.y);
}
// 结束绘制(恢复上下文状态)
end() {
this.ctx.restore();
}
}
// 使用示例
const canvas = document.getElementById('canvas');
const worldWidth = 10000;
const worldHeight = 10000;
const viewport = new Viewport(canvas, worldWidth, worldHeight);
// 创建一些游戏对象
const gameObjects = [];
for (let i = 0; i < 10000; i++) {
gameObjects.push({
x: Math.random() * worldWidth,
y: Math.random() * worldHeight,
width: 20 + Math.random() * 50,
height: 20 + Math.random() * 50,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
});
}
// 渲染函数
function render() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
viewport.width = canvas.width;
viewport.height = canvas.height;
// 清除画布
viewport.ctx.clearRect(0, 0, canvas.width, canvas.height);
// 开始应用视口变换
viewport.begin();
// 绘制世界边界
viewport.ctx.strokeStyle = 'black';
viewport.ctx.strokeRect(0, 0, worldWidth, worldHeight);
// 只绘制视口内的对象
let visibleCount = 0;
let totalCount = gameObjects.length;
gameObjects.forEach(obj => {
if (viewport.isVisible(obj.x, obj.y, obj.width, obj.height)) {
viewport.ctx.fillStyle = obj.color;
viewport.ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
visibleCount++;
}
});
// 结束视口变换
viewport.end();
// 绘制UI(不受视口影响)
viewport.ctx.fillStyle = 'black';
viewport.ctx.font = '14px Arial';
viewport.ctx.fillText(`可见对象: ${visibleCount}/${totalCount}`, 10, 20);
viewport.ctx.fillText(`视口位置: (${viewport.x.toFixed(0)}, ${viewport.y.toFixed(0)})`, 10, 40);
viewport.ctx.fillText(`缩放: ${viewport.scale.toFixed(2)}x`, 10, 60);
requestAnimationFrame(render);
}
// 处理输入
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
});
canvas.addEventListener('mousemove', (e) => {
if (isDragging) {
const dx = (lastMouseX - e.clientX) / viewport.scale;
const dy = (lastMouseY - e.clientY) / viewport.scale;
viewport.move(dx, dy);
lastMouseX = e.clientX;
lastMouseY = e.clientY;
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.1 : 0.9;
viewport.zoom(factor, e.clientX, e.clientY);
});
// 开始渲染
render();
10.4.2 四叉树空间分区
对于大型场景,使用四叉树可以加速碰撞检测和可见性检测:
class QuadTree {
constructor(boundary, capacity) {
this.boundary = boundary; // {x, y, width, height}
this.capacity = capacity; // 每个节点的最大对象数
this.objects = [];
this.divided = false;
this.children = [];
}
// 细分四叉树
subdivide() {
const x = this.boundary.x;
const y = this.boundary.y;
const w = this.boundary.width / 2;
const h = this.boundary.height / 2;
// 创建四个子节点
this.children = [
new QuadTree({x: x, y: y, width: w, height: h}, this.capacity), // 左上
new QuadTree({x: x + w, y: y, width: w, height: h}, this.capacity), // 右上
new QuadTree({x: x, y: y + h, width: w, height: h}, this.capacity), // 左下
new QuadTree({x: x + w, y: y + h, width: w, height: h}, this.capacity) // 右下
];
this.divided = true;
// 将现有对象重新分配到子节点
for (let obj of this.objects) {
this.insertToChildren(obj);
}
this.objects = [];
}
// 将对象插入到子节点
insertToChildren(obj) {
for (let child of this.children) {
if (this.intersects(obj, child.boundary)) {
child.insert(obj);
}
}
}
// 插入对象
insert(obj) {
// 如果对象不在边界内,不插入
if (!this.intersects(obj, this.boundary)) {
return false;
}
// 如果当前节点未满且未细分,直接添加
if (this.objects.length < this.capacity && !this.divided) {
this.objects.push(obj);
return true;
}
// 如果未细分,则细分
if (!this.divided) {
this.subdivide();
}
// 将对象插入到子节点
return this.insertToChildren(obj);
}
// 查询给定范围内的所有对象
query(range, found = []) {
// 如果查询范围与当前节点不相交,返回空数组
if (!this.intersects(range, this.boundary)) {
return found;
}
// 添加当前节点中的对象
for (let obj of this.objects) {
if (this.intersects(obj, range)) {
found.push(obj);
}
}
// 如果已细分,递归查询子节点
if (this.divided) {
for (let child of this.children) {
child.query(range, found);
}
}
return found;
}
// 检查两个矩形是否相交
intersects(rectA, rectB) {
return (
rectA.x < rectB.x + rectB.width &&
rectA.x + rectA.width > rectB.x &&
rectA.y < rectB.y + rectB.height &&
rectA.y + rectA.height > rectB.y
);
}
// 清空四叉树
clear() {
this.objects = [];
if (this.divided) {
for (let child of this.children) {
child.clear();
}
this.children = [];
this.divided = false;
}
}
// 绘制四叉树(用于调试)
draw(ctx) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.strokeRect(
this.boundary.x,
this.boundary.y,
this.boundary.width,
this.boundary.height
);
if (this.divided) {
for (let child of this.children) {
child.draw(ctx);
}
}
}
}
// 使用示例
const worldWidth = 2000;
const worldHeight = 2000;
const quadTree = new QuadTree({x: 0, y: 0, width: worldWidth, height: worldHeight}, 4);
// 创建一些游戏对象
const gameObjects = [];
for (let i = 0; i < 1000; i++) {
const obj = {
id: i,
x: Math.random() * worldWidth,
y: Math.random() * worldHeight,
width: 10 + Math.random() * 40,
height: 10 + Math.random() * 40,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
};
gameObjects.push(obj);
}
// 在每一帧重建四叉树
function updateQuadTree() {
quadTree.clear();
for (let obj of gameObjects) {
quadTree.insert(obj);
}
}
// 使用四叉树进行可见性检测
function getVisibleObjects(viewport) {
const viewportBounds = {
x: viewport.x,
y: viewport.y,
width: viewport.width / viewport.scale,
height: viewport.height / viewport.scale
};
return quadTree.query(viewportBounds);
}
// 渲染函数
function render() {
// 更新四叉树
updateQuadTree();
// 获取可见对象
const visibleObjects = getVisibleObjects(viewport);
// 清除画布
viewport.ctx.clearRect(0, 0, canvas.width, canvas.height);
// 开始应用视口变换
viewport.begin();
// 绘制世界边界
viewport.ctx.strokeStyle = 'black';
viewport.ctx.strokeRect(0, 0, worldWidth, worldHeight);
// 绘制四叉树(调试用)
quadTree.draw(viewport.ctx);
// 绘制可见对象
for (let obj of visibleObjects) {
viewport.ctx.fillStyle = obj.color;
viewport.ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
}
// 结束视口变换
viewport.end();
// 绘制UI
viewport.ctx.fillStyle = 'black';
viewport.ctx.font = '14px Arial';
viewport.ctx.fillText(`可见对象: ${visibleObjects.length}/${gameObjects.length}`, 10, 20);
requestAnimationFrame(render);
}
10.5 Canvas应用最佳实践
10.5.1 响应式Canvas
创建适应不同屏幕尺寸的Canvas应用:
class ResponsiveCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
// 设置初始尺寸
this.resize();
// 监听窗口大小变化
window.addEventListener('resize', this.resize.bind(this));
// 设备像素比
this.pixelRatio = window.devicePixelRatio || 1;
// 应用高DPI支持
this.applyHighDPI();
}
// 调整Canvas尺寸
resize() {
const parent = this.canvas.parentElement;
// 如果父元素存在,使用父元素尺寸,否则使用窗口尺寸
if (parent) {
this.canvas.width = parent.clientWidth;
this.canvas.height = parent.clientHeight;
} else {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
// 重新应用高DPI支持
this.applyHighDPI();
// 触发重绘
this.draw();
}
// 应用高DPI支持
applyHighDPI() {
if (this.pixelRatio > 1) {
const width = this.canvas.width;
const height = this.canvas.height;
// 设置Canvas元素的显示尺寸
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
// 增加Canvas的实际像素数
this.canvas.width = width * this.pixelRatio;
this.canvas.height = height * this.pixelRatio;
// 缩放上下文以匹配设备像素比
this.ctx.scale(this.pixelRatio, this.pixelRatio);
}
}
// 绘制方法(由子类实现)
draw() {
// 默认实现:清除画布
this.ctx.clearRect(0, 0, this.canvas.width / this.pixelRatio, this.canvas.height / this.pixelRatio);
}
// 获取Canvas中心点
getCenter() {
return {
x: this.canvas.width / this.pixelRatio / 2,
y: this.canvas.height / this.pixelRatio / 2
};
}
// 获取Canvas尺寸
getSize() {
return {
width: this.canvas.width / this.pixelRatio,
height: this.canvas.height / this.pixelRatio
};
}
}
// 使用示例
class MyCanvasApp extends ResponsiveCanvas {
constructor(canvasId) {
super(canvasId);
// 初始化应用状态
this.circles = [];
for (let i = 0; i < 50; i++) {
this.circles.push({
x: Math.random() * this.getSize().width,
y: Math.random() * this.getSize().height,
radius: 10 + Math.random() * 40,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
});
}
// 开始动画循环
this.animate();
}
// 动画循环
animate() {
requestAnimationFrame(this.animate.bind(this));
this.draw();
}
// 重写绘制方法
draw() {
// 清除画布
super.draw();
// 获取当前Canvas尺寸
const size = this.getSize();
// 绘制背景
const gradient = this.ctx.createLinearGradient(0, 0, 0, size.height);
gradient.addColorStop(0, '#f5f7fa');
gradient.addColorStop(1, '#c3cfe2');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, 0, size.width, size.height);
// 绘制圆形
this.circles.forEach(circle => {
// 确保圆形在Canvas范围内
if (circle.x < -circle.radius) circle.x = size.width + circle.radius;
if (circle.x > size.width + circle.radius) circle.x = -circle.radius;
if (circle.y < -circle.radius) circle.y = size.height + circle.radius;
if (circle.y > size.height + circle.radius) circle.y = -circle.radius;
// 移动圆形
circle.x += (Math.random() - 0.5) * 2;
circle.y += (Math.random() - 0.5) * 2;
// 绘制圆形
this.ctx.beginPath();
this.ctx.fillStyle = circle.color;
this.ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
this.ctx.fill();
});
// 绘制文本
const center = this.getCenter();
this.ctx.font = 'bold 24px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillStyle = 'white';
this.ctx.fillText('响应式Canvas示例', center.x, center.y);
}
}
// 创建应用实例
const app = new MyCanvasApp('canvas');
10.5.2 模块化与架构设计
对于复杂的Canvas应用,采用模块化架构可以提高代码的可维护性:
// 游戏引擎架构示例
// 基础游戏对象类
class GameObject {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rotation = 0;
this.scale = { x: 1, y: 1 };
this.visible = true;
this.children = [];
}
update(deltaTime) {
// 更新子对象
this.children.forEach(child => child.update(deltaTime));
}
draw(ctx) {
if (!this.visible) return;
ctx.save();
// 应用变换
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.scale(this.scale.x, this.scale.y);
// 绘制自身
this.drawSelf(ctx);
// 绘制子对象
this.children.forEach(child => child.draw(ctx));
ctx.restore();
}
drawSelf(ctx) {
// 由子类实现
}
addChild(gameObject) {
this.children.push(gameObject);
}
removeChild(gameObject) {
const index = this.children.indexOf(gameObject);
if (index !== -1) {
this.children.splice(index, 1);
}
}
}
// 场景类
class Scene extends GameObject {
constructor() {
super(0, 0, 0, 0);
this.layers = {};
}
addToLayer(layerName, gameObject) {
if (!this.layers[layerName]) {
this.layers[layerName] = [];
}
this.layers[layerName].push(gameObject);
}
update(deltaTime) {
// 更新所有层中的对象
Object.values(this.layers).forEach(layer => {
layer.forEach(obj => obj.update(deltaTime));
});
}
draw(ctx) {
// 按层绘制对象
Object.keys(this.layers).forEach(layerName => {
this.layers[layerName].forEach(obj => obj.draw(ctx));
});
}
}
// 游戏引擎类
class GameEngine {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
// 设置Canvas尺寸
this.resizeCanvas();
window.addEventListener('resize', this.resizeCanvas.bind(this));
// 游戏状态
this.scenes = {};
this.currentScene = null;
// 时间管理
this.lastTime = 0;
this.deltaTime = 0;
// 输入管理
this.input = new InputManager(this.canvas);
// 资源管理
this.resources = new ResourceManager();
// 性能监控
this.perfMonitor = new PerformanceMonitor();
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
addScene(name, scene) {
this.scenes[name] = scene;
}
setCurrentScene(name) {
if (this.scenes[name]) {
this.currentScene = this.scenes[name];
}
}
start() {
// 开始游戏循环
requestAnimationFrame(this.gameLoop.bind(this));
}
gameLoop(timestamp) {
// 计算时间增量
this.deltaTime = (timestamp - this.lastTime) / 1000; // 转换为秒
this.lastTime = timestamp;
// 开始性能监控
this.perfMonitor.startFrame();
// 更新输入状态
this.input.update();
// 更新当前场景
if (this.currentScene) {
this.currentScene.update(this.deltaTime);
}
// 清除画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制当前场景
if (this.currentScene) {
this.currentScene.draw(this.ctx);
}
// 结束性能监控并显示
this.perfMonitor.endFrame(this.ctx, 10, 20);
// 继续游戏循环
requestAnimationFrame(this.gameLoop.bind(this));
}
}
// 输入管理器
class InputManager {
constructor(canvas) {
this.canvas = canvas;
// 键盘状态
this.keys = {};
// 鼠标状态
this.mousePosition = { x: 0, y: 0 };
this.mouseButtons = { left: false, middle: false, right: false };
// 触摸状态
this.touches = [];
// 注册事件监听器
this.registerEventListeners();
}
registerEventListeners() {
// 键盘事件
window.addEventListener('keydown', this.handleKeyDown.bind(this));
window.addEventListener('keyup', this.handleKeyUp.bind(this));
// 鼠标事件
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
// 触摸事件
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
}
// 键盘事件处理
handleKeyDown(e) {
this.keys[e.code] = true;
}
handleKeyUp(e) {
this.keys[e.code] = false;
}
// 鼠标事件处理
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
this.mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
handleMouseDown(e) {
switch (e.button) {
case 0: this.mouseButtons.left = true; break;
case 1: this.mouseButtons.middle = true; break;
case 2: this.mouseButtons.right = true; break;
}
}
handleMouseUp(e) {
switch (e.button) {
case 0: this.mouseButtons.left = false; break;
case 1: this.mouseButtons.middle = false; break;
case 2: this.mouseButtons.right = false; break;
}
}
// 触摸事件处理
handleTouchStart(e) {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
this.touches = Array.from(e.touches).map(touch => ({
id: touch.identifier,
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
}));
// 模拟鼠标左键按下
if (this.touches.length > 0) {
this.mouseButtons.left = true;
this.mousePosition.x = this.touches[0].x;
this.mousePosition.y = this.touches[0].y;
}
}
handleTouchMove(e) {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
this.touches = Array.from(e.touches).map(touch => ({
id: touch.identifier,
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
}));
// 更新鼠标位置
if (this.touches.length > 0) {
this.mousePosition.x = this.touches[0].x;
this.mousePosition.y = this.touches[0].y;
}
}
handleTouchEnd(e) {
e.preventDefault();
// 更新触摸列表
const activeTouchIds = Array.from(e.touches).map(touch => touch.identifier);
this.touches = this.touches.filter(touch => activeTouchIds.includes(touch.id));
// 如果没有触摸点,模拟鼠标左键释放
if (this.touches.length === 0) {
this.mouseButtons.left = false;
}
}
// 检查按键是否按下
isKeyDown(keyCode) {
return this.keys[keyCode] === true;
}
// 检查鼠标按钮是否按下
isMouseButtonDown(button) {
return this.mouseButtons[button] === true;
}
// 获取鼠标位置
getMousePosition() {
return { ...this.mousePosition };
}
// 获取触摸点
getTouches() {
return [...this.touches];
}
// 更新方法(每帧调用)
update() {
// 可以在这里添加额外的输入处理逻辑
}
}
// 资源管理器
class ResourceManager {
constructor() {
this.resources = {
images: {},
audio: {},
json: {}
};
this.loadingPromises = [];
}
// 加载图像
loadImage(key, url) {
const promise = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this.resources.images[key] = img;
resolve(img);
};
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
this.loadingPromises.push(promise);
return promise;
}
// 加载音频
loadAudio(key, url) {
const promise = new Promise((resolve, reject) => {
const audio = new Audio();
audio.oncanplaythrough = () => {
this.resources.audio[key] = audio;
resolve(audio);
};
audio.onerror = () => reject(new Error(`Failed to load audio: ${url}`));
audio.src = url;
});
this.loadingPromises.push(promise);
return promise;
}
// 加载JSON
loadJSON(key, url) {
const promise = fetch(url)
.then(response => response.json())
.then(data => {
this.resources.json[key] = data;
return data;
});
this.loadingPromises.push(promise);
return promise;
}
// 等待所有资源加载完成
waitForAll() {
return Promise.all(this.loadingPromises);
}
// 获取资源
getImage(key) {
return this.resources.images[key];
}
getAudio(key) {
return this.resources.audio[key];
}
getJSON(key) {
return this.resources.json[key];
}
}
// 使用示例:创建一个简单的游戏
class SimpleGame {
constructor(canvasId) {
// 创建游戏引擎
this.engine = new GameEngine(canvasId);
// 加载资源
this.loadResources();
}
async loadResources() {
// 加载游戏资源
this.engine.resources.loadImage('player', 'assets/player.png');
this.engine.resources.loadImage('enemy', 'assets/enemy.png');
this.engine.resources.loadImage('background', 'assets/background.png');
try {
// 等待所有资源加载完成
await this.engine.resources.waitForAll();
// 初始化游戏
this.initGame();
} catch (error) {
console.error('资源加载失败:', error);
}
}
initGame() {
// 创建游戏场景
const mainScene = new Scene();
// 添加背景
const background = new BackgroundObject(
0, 0,
this.engine.canvas.width,
this.engine.canvas.height,
this.engine.resources.getImage('background')
);
mainScene.addToLayer('background', background);
// 添加玩家
const player = new PlayerObject(
this.engine.canvas.width / 2,
this.engine.canvas.height / 2,
50, 50,
this.engine.resources.getImage('player')
);
mainScene.addToLayer('entities', player);
// 添加敌人
for (let i = 0; i < 10; i++) {
const enemy = new EnemyObject(
Math.random() * this.engine.canvas.width,
Math.random() * this.engine.canvas.height,
40, 40,
this.engine.resources.getImage('enemy')
);
mainScene.addToLayer('entities', enemy);
}
// 添加UI
const ui = new UIObject(10, 10);
mainScene.addToLayer('ui', ui);
// 将场景添加到引擎
this.engine.addScene('main', mainScene);
this.engine.setCurrentScene('main');
// 开始游戏循环
this.engine.start();
}
}
// 背景对象
class BackgroundObject extends GameObject {
constructor(x, y, width, height, image) {
super(x, y, width, height);
this.image = image;
}
drawSelf(ctx) {
if (this.image) {
ctx.drawImage(this.image, 0, 0, this.width, this.height);
} else {
// 如果图像未加载,绘制一个简单的背景
const gradient = ctx.createLinearGradient(0, 0, 0, this.height);
gradient.addColorStop(0, '#87CEEB');
gradient.addColorStop(1, '#E0F7FA');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.width, this.height);
}
}
}
// 玩家对象
class PlayerObject extends GameObject {
constructor(x, y, width, height, image) {
super(x, y, width, height);
this.image = image;
this.speed = 200; // 像素/秒
}
update(deltaTime) {
// 获取输入管理器
const input = this.scene.engine.input;
// 处理键盘输入
let dx = 0;
let dy = 0;
if (input.isKeyDown('ArrowUp') || input.isKeyDown('KeyW')) {
dy -= this.speed * deltaTime;
}
if (input.isKeyDown('ArrowDown') || input.isKeyDown('KeyS')) {
dy += this.speed * deltaTime;
}
if (input.isKeyDown('ArrowLeft') || input.isKeyDown('KeyA')) {
dx -= this.speed * deltaTime;
}
if (input.isKeyDown('ArrowRight') || input.isKeyDown('KeyD')) {
dx += this.speed * deltaTime;
}
// 更新位置
this.x += dx;
this.y += dy;
// 限制在画布范围内
const canvas = this.scene.engine.canvas;
this.x = Math.max(0, Math.min(this.x, canvas.width - this.width));
this.y = Math.max(0, Math.min(this.y, canvas.height - this.height));
super.update(deltaTime);
}
drawSelf(ctx) {
if (this.image) {
ctx.drawImage(this.image, -this.width / 2, -this.height / 2, this.width, this.height);
} else {
// 如果图像未加载,绘制一个简单的形状
ctx.fillStyle = 'blue';
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
}
}
// 敌人对象
class EnemyObject extends GameObject {
constructor(x, y, width, height, image) {
super(x, y, width, height);
this.image = image;
this.speed = 100; // 像素/秒
this.rotationSpeed = Math.PI; // 弧度/秒
}
update(deltaTime) {
// 旋转
this.rotation += this.rotationSpeed * deltaTime;
// 简单的移动模式
this.x += Math.cos(this.rotation) * this.speed * deltaTime;
this.y += Math.sin(this.rotation) * this.speed * deltaTime;
// 如果超出边界,反弹
const canvas = this.scene.engine.canvas;
if (this.x < 0 || this.x > canvas.width - this.width) {
this.speed *= -1;
}
if (this.y < 0 || this.y > canvas.height - this.height) {
this.speed *= -1;
}
super.update(deltaTime);
}
drawSelf(ctx) {
if (this.image) {
ctx.drawImage(this.image, -this.width / 2, -this.height / 2, this.width, this.height);
} else {
// 如果图像未加载,绘制一个简单的形状
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.moveTo(0, -this.height / 2);
ctx.lineTo(this.width / 2, this.height / 2);
ctx.lineTo(-this.width / 2, this.height / 2);
ctx.closePath();
ctx.fill();
}
}
}
// UI对象
class UIObject extends GameObject {
constructor(x, y) {
super(x, y, 0, 0);
this.fps = 0;
this.frameCount = 0;
this.lastFpsUpdate = 0;
}
update(deltaTime) {
// 更新FPS计数
this.frameCount++;
const now = performance.now();
if (now - this.lastFpsUpdate >= 1000) {
this.fps = Math.round((this.frameCount * 1000) / (now - this.lastFpsUpdate));
this.frameCount = 0;
this.lastFpsUpdate = now;
}
super.update(deltaTime);
}
drawSelf(ctx) {
ctx.font = '16px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(`FPS: ${this.fps}`, 0, 0);
ctx.fillText(`按方向键或WASD移动`, 0, 20);
}
}
// 创建游戏实例
// const game = new SimpleGame('canvas');
10.6 小结与最佳实践
在本章中,我们学习了Canvas性能优化和最佳实践的关键技术。以下是一些重要的总结点:
10.6.1 性能优化核心原则
- 测量先于优化:使用性能分析工具确定瓶颈,避免过早优化。
- 减少状态更改:批量处理相似的绘图操作,减少Canvas状态切换。
- 使用分层渲染:将静态和动态内容分离,减少重绘区域。
- 离屏渲染与缓存:预渲染复杂图形,减少每帧的绘制开销。
- 视口裁剪:只渲染可见区域内的对象,使用空间分区技术优化大型场景。
- 内存管理:使用对象池和TypedArray减少垃圾回收,避免在动画循环中创建临时对象。
- 选择合适的API:使用最高效的Canvas API进行绘图操作。
10.6.2 架构设计最佳实践
- 模块化设计:将游戏或应用分解为独立的组件,提高可维护性。
- 资源管理:集中管理和预加载资源,避免运行时加载导致的卡顿。
- 响应式设计:适应不同屏幕尺寸和设备像素比,提供一致的用户体验。
- 输入抽象:统一处理键盘、鼠标和触摸输入,简化交互逻辑。
10.6.3 调试与优化工具
- 浏览器开发者工具:使用Performance和Memory面板分析性能问题。
- 自定义性能监控:实现FPS计数器和渲染时间监控,实时评估性能。
- 可视化调试:绘制碰撞边界、四叉树分区等辅助信息,帮助调试复杂问题。
10.6.4 常见性能陷阱
- 过度绘制:绘制不可见区域或重复绘制相同区域。
- 频繁的DOM操作:在动画循环中修改DOM或调整Canvas大小。
- 复杂路径:使用过于复杂的路径或不必要的曲线。
- 未优化的图像:使用过大的图像或未使用适当的图像格式。
- 忽略设备像素比:在高DPI设备上未正确缩放Canvas,导致模糊或性能问题。
10.7 练习
性能分析工具:实现一个完整的性能监控面板,显示FPS、渲染时间、内存使用和对象计数。
优化粒子系统:使用本章学习的技术优化粒子系统,使其能够高效地渲染10000+粒子。
响应式游戏:创建一个在不同设备上都能良好运行的Canvas游戏,包括桌面和移动设备。
四叉树碰撞检测:实现一个使用四叉树的碰撞检测系统,并与暴力检测方法进行性能比较。
模块化游戏引擎:基于本章的架构示例,扩展实现一个完整的2D游戏引擎,包括场景管理、物理系统和动画系统。
下一章预告
在下一章中,我们将学习Canvas与其他Web技术的集成,包括与WebGL、SVG、CSS和Web组件的结合使用,以及如何将Canvas应用部署到生产环境。