1. H5游戏开发基础:Canvas与JS的黄金组合
十年前我刚接触H5游戏开发时,Canvas和JavaScript的组合就像发现新大陆一样令人兴奋。如今这套技术栈已经成为移动端轻量级游戏开发的标准解决方案,从微信小游戏到营销活动页面,处处都能看到它的身影。
Canvas本质上是一块画布,通过JavaScript指令控制绘制内容。这种模式特别适合需要频繁重绘的场景——比如游戏中的角色移动、碰撞检测、动画效果等。与DOM操作相比,Canvas的性能优势明显,这也是为什么它能成为H5游戏开发的首选方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Canvas绘图核心原理剖析
2.1 坐标系与绘制上下文
Canvas使用标准的二维笛卡尔坐标系,原点(0,0)位于画布左上角。获取绘图上下文是这个过程中最关键的一步:
javascript复制const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
这个ctx对象就是我们的魔法画笔,它提供了全套绘图API。我建议在项目初期就封装好绘图工具函数,比如下面这个绘制圆角的矩形方法:
javascript复制function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// 其余三个角的绘制...
ctx.fill();
}
2.2 动画循环的实现机制
游戏的核心是动画循环,在Canvas中我们通常使用requestAnimationFrame:
javascript复制function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新游戏状态
updateGame();
// 绘制游戏元素
renderGame();
requestAnimationFrame(gameLoop);
}
重要提示:一定要在每次绘制前清除整个画布,否则会出现画面残留。我在早期项目中就犯过这个错误,导致角色移动时拖出了长长的"尾巴"。
3. 游戏开发中的性能优化技巧
3.1 离屏Canvas技术
当需要频繁绘制相同元素时(比如大量相同类型的敌人),使用离屏Canvas可以显著提升性能:
javascript复制// 创建离屏Canvas
const offScreenCanvas = document.createElement('canvas');
const offScreenCtx = offScreenCanvas.getContext('2d');
// 在离屏Canvas上绘制
offScreenCtx.fillStyle = 'red';
offScreenCtx.fillRect(0, 0, 50, 50);
// 主Canvas中重复使用
function render() {
ctx.drawImage(offScreenCanvas, x, y);
}
3.2 脏矩形渲染策略
对于复杂场景,只重绘发生变化的部分区域:
javascript复制let dirtyAreas = [];
function addDirtyArea(x, y, width, height) {
dirtyAreas.push({x, y, width, height});
}
function render() {
if(dirtyAreas.length === 0) return;
dirtyAreas.forEach(area => {
ctx.clearRect(area.x, area.y, area.width, area.height);
// 只重绘该区域内的元素
});
dirtyAreas = [];
}
4. 常见问题与解决方案
4.1 跨域资源加载问题
当尝试加载外部图片资源时,可能会遇到跨域限制:
javascript复制const img = new Image();
img.crossOrigin = "Anonymous"; // 关键设置
img.src = "https://example.com/game-asset.png";
如果服务端不支持CORS,可以考虑将图片转为Base64编码或使用代理方案。
4.2 移动端触摸事件处理
移动端需要特别处理触摸事件:
javascript复制canvas.addEventListener('touchstart', handleTouch);
canvas.addEventListener('touchmove', handleTouch);
function handleTouch(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(mouseEvent);
}
5. 游戏开发完整实例
下面是一个简单的弹球游戏框架:
javascript复制// 游戏状态
const game = {
ball: { x: 100, y: 100, dx: 2, dy: 2, radius: 10 },
paddle: { x: 0, y: 380, width: 75, height: 10 }
};
function update() {
// 球体运动
game.ball.x += game.ball.dx;
game.ball.y += game.ball.dy;
// 碰撞检测
if(game.ball.x + game.ball.radius > canvas.width ||
game.ball.x - game.ball.radius < 0) {
game.ball.dx = -game.ball.dx;
}
// 与挡板碰撞
if(game.ball.y + game.ball.radius > game.paddle.y &&
game.ball.x > game.paddle.x &&
game.ball.x < game.paddle.x + game.paddle.width) {
game.ball.dy = -game.ball.dy;
}
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制球
ctx.beginPath();
ctx.arc(game.ball.x, game.ball.y, game.ball.radius, 0, Math.PI*2);
ctx.fill();
// 绘制挡板
ctx.fillRect(game.paddle.x, game.paddle.y, game.paddle.width, game.paddle.height);
}
6. 进阶开发技巧
6.1 使用WebGL提升性能
对于复杂游戏,可以考虑使用WebGL渲染:
javascript复制const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
// 初始化着色器程序
const shaderProgram = gl.createProgram();
// ... 着色器代码和链接过程
6.2 游戏状态管理
随着游戏复杂度增加,需要引入状态管理:
javascript复制const gameStates = {
MENU: 0,
PLAYING: 1,
GAME_OVER: 2
};
let currentState = gameStates.MENU;
function update() {
switch(currentState) {
case gameStates.MENU:
updateMenu();
break;
case gameStates.PLAYING:
updateGame();
break;
// 其他状态...
}
}
7. 调试与性能分析
7.1 使用Chrome开发者工具
Chrome的Performance面板可以分析游戏运行时的性能瓶颈:
- 打开开发者工具(Control+Shift+I)
- 切换到Performance面板
- 点击录制按钮
- 操作游戏
- 停止录制并分析结果
重点关注:
- 脚本执行时间
- 渲染时间
- 内存使用情况
7.2 帧率监控
实现简单的FPS计数器:
javascript复制let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
function updateFPS() {
const now = performance.now();
frameCount++;
if(now - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = now;
}
ctx.fillStyle = 'black';
ctx.fillText(`FPS: ${fps}`, 10, 20);
}
8. 游戏资源管理
8.1 资源预加载系统
实现一个简单的资源加载器:
javascript复制class AssetLoader {
constructor() {
this.assets = {};
this.loaded = 0;
this.total = 0;
}
loadImage(key, url) {
this.total++;
const img = new Image();
img.onload = () => {
this.assets[key] = img;
this.loaded++;
if(this.loaded === this.total) {
this.onComplete();
}
};
img.src = url;
}
onComplete() {
console.log('所有资源加载完成');
}
}
8.2 精灵图(Sprite Sheet)使用
优化小图像资源加载:
javascript复制function drawSprite(sprite, x, y, frame) {
const frameWidth = sprite.width / sprite.cols;
const frameHeight = sprite.height / sprite.rows;
const col = frame % sprite.cols;
const row = Math.floor(frame / sprite.cols);
ctx.drawImage(
sprite.image,
col * frameWidth,
row * frameHeight,
frameWidth,
frameHeight,
x,
y,
frameWidth,
frameHeight
);
}
9. 游戏物理系统基础
9.1 简单碰撞检测
矩形与矩形碰撞:
javascript复制function rectCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
圆形与圆形碰撞:
javascript复制function circleCollision(circle1, circle2) {
const dx = circle1.x - circle2.x;
const dy = circle1.y - circle2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < circle1.radius + circle2.radius;
}
9.2 简单物理运动
带加速度的运动模型:
javascript复制class GameObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.ax = 0;
this.ay = 0;
}
update() {
this.vx += this.ax;
this.vy += this.ay;
this.x += this.vx;
this.y += this.vy;
}
}
10. 游戏音效处理
10.1 Web Audio API基础
创建音频上下文:
javascript复制const audioContext = new (window.AudioContext || window.webkitAudioContext)();
function playSound(buffer) {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
}
10.2 音效池技术
避免频繁创建音频对象:
javascript复制class SoundPool {
constructor(url, size) {
this.pool = [];
this.index = 0;
for(let i = 0; i < size; i++) {
const audio = new Audio(url);
this.pool.push(audio);
}
}
play() {
this.pool[this.index].currentTime = 0;
this.pool[this.index].play();
this.index = (this.index + 1) % this.pool.length;
}
}
11. 游戏存档与本地存储
11.1 使用localStorage
保存游戏进度:
javascript复制function saveGame() {
const gameData = {
level: currentLevel,
score: playerScore,
inventory: playerInventory
};
localStorage.setItem('gameSave', JSON.stringify(gameData));
}
function loadGame() {
const savedData = localStorage.getItem('gameSave');
if(savedData) {
const gameData = JSON.parse(savedData);
currentLevel = gameData.level;
playerScore = gameData.score;
// 恢复其他游戏状态...
}
}
11.2 数据压缩技巧
对于大型游戏数据,可以考虑压缩:
javascript复制function compressData(data) {
const str = JSON.stringify(data);
return LZString.compressToUTF16(str);
}
function decompressData(compressed) {
const str = LZString.decompressFromUTF16(compressed);
return JSON.parse(str);
}
12. 游戏发布与优化
12.1 资源打包与压缩
使用工具如Webpack打包游戏资源:
javascript复制// webpack.config.js
module.exports = {
entry: './src/game.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: 'assets/'
}
}
]
}
]
}
};
12.2 移动端适配技巧
处理不同屏幕尺寸:
javascript复制function resizeCanvas() {
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
// 调整游戏元素位置和大小
game.paddle.width = width * 0.2;
game.paddle.y = height - 20;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
13. 游戏测试与调试
13.1 自动化测试框架
使用Jest进行单元测试:
javascript复制// collision.test.js
test('rectangles should collide', () => {
const rect1 = { x: 0, y: 0, width: 10, height: 10 };
const rect2 = { x: 5, y: 5, width: 10, height: 10 };
expect(collision(rect1, rect2)).toBe(true);
});
13.2 游戏状态快照
保存和恢复游戏状态用于调试:
javascript复制let gameSnapshot = null;
function takeSnapshot() {
gameSnapshot = JSON.parse(JSON.stringify(gameState));
}
function restoreSnapshot() {
if(gameSnapshot) {
gameState = JSON.parse(JSON.stringify(gameSnapshot));
}
}
14. 游戏性能监控
14.1 内存使用监控
检测内存泄漏:
javascript复制function checkMemory() {
const memory = window.performance.memory;
console.log(`Used JS heap size: ${memory.usedJSHeapSize / 1024 / 1024} MB`);
}
14.2 渲染性能分析
测量帧渲染时间:
javascript复制let lastFrameTime = performance.now();
function render() {
const startTime = performance.now();
// 渲染代码...
const renderTime = performance.now() - startTime;
console.log(`Frame render time: ${renderTime}ms`);
lastFrameTime = performance.now();
}
15. 游戏AI基础
15.1 简单敌人AI
追逐玩家算法:
javascript复制function chasePlayer(enemy, player) {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if(distance > 0) {
enemy.vx = (dx / distance) * enemy.speed;
enemy.vy = (dy / distance) * enemy.speed;
}
}
15.2 有限状态机
实现敌人行为状态:
javascript复制class Enemy {
constructor() {
this.state = 'idle';
this.states = {
idle: this.idleState.bind(this),
chase: this.chaseState.bind(this),
attack: this.attackState.bind(this)
};
}
update() {
this.states[this.state]();
}
idleState() {
// 闲置行为
if(seePlayer()) {
this.state = 'chase';
}
}
// 其他状态方法...
}
16. 游戏特效实现
16.1 粒子系统
创建爆炸效果:
javascript复制class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = Math.random() * 4 - 2;
this.vy = Math.random() * 4 - 2;
this.alpha = 1;
this.size = Math.random() * 3 + 1;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.alpha -= 0.02;
}
draw(ctx) {
ctx.globalAlpha = this.alpha;
ctx.fillStyle = 'orange';
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.globalAlpha = 1;
}
}
class ParticleSystem {
constructor() {
this.particles = [];
}
createExplosion(x, y, count) {
for(let i = 0; i < count; i++) {
this.particles.push(new Particle(x, y));
}
}
update() {
for(let i = this.particles.length - 1; i >= 0; i--) {
this.particles[i].update();
if(this.particles[i].alpha <= 0) {
this.particles.splice(i, 1);
}
}
}
render(ctx) {
this.particles.forEach(p => p.draw(ctx));
}
}
16.2 光影效果
实现简单的光照:
javascript复制function applyLighting(ctx, x, y, radius, intensity) {
const gradient = ctx.createRadialGradient(
x, y, 0,
x, y, radius
);
gradient.addColorStop(0, `rgba(255, 255, 255, ${intensity})`);
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
}
17. 游戏UI系统
17.1 按钮组件实现
创建可交互按钮:
javascript复制class Button {
constructor(x, y, width, height, text, onClick) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.text = text;
this.onClick = onClick;
this.isHovered = false;
}
draw(ctx) {
ctx.fillStyle = this.isHovered ? '#555' : '#333';
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.text, this.x + this.width/2, this.y + this.height/2);
}
checkHover(mouseX, mouseY) {
this.isHovered = (
mouseX >= this.x &&
mouseX <= this.x + this.width &&
mouseY >= this.y &&
mouseY <= this.y + this.height
);
return this.isHovered;
}
handleClick() {
if(this.isHovered && this.onClick) {
this.onClick();
}
}
}
17.2 进度条组件
实现血条/进度条:
javascript复制class ProgressBar {
constructor(x, y, width, height, maxValue, color) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.maxValue = maxValue;
this.currentValue = maxValue;
this.color = color;
}
setValue(value) {
this.currentValue = Math.max(0, Math.min(value, this.maxValue));
}
draw(ctx) {
// 背景
ctx.fillStyle = '#333';
ctx.fillRect(this.x, this.y, this.width, this.height);
// 进度
const progressWidth = (this.currentValue / this.maxValue) * this.width;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, progressWidth, this.height);
// 边框
ctx.strokeStyle = '#000';
ctx.strokeRect(this.x, this.y, this.width, this.height);
}
}
18. 游戏输入系统
18.1 键盘输入管理
处理多键同时按下:
javascript复制class InputManager {
constructor() {
this.keys = {};
window.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
});
window.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
isKeyDown(key) {
return this.keys[key] || false;
}
}
const input = new InputManager();
function update() {
if(input.isKeyDown('ArrowLeft')) {
player.x -= player.speed;
}
if(input.isKeyDown('ArrowRight')) {
player.x += player.speed;
}
}
18.2 虚拟摇杆实现
移动端虚拟摇杆:
javascript复制class VirtualJoystick {
constructor() {
this.baseX = 0;
this.baseY = 0;
this.thumbX = 0;
this.thumbY = 0;
this.radius = 50;
this.isActive = false;
this.setupEvents();
}
setupEvents() {
canvas.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
this.baseX = touch.clientX;
this.baseY = touch.clientY;
this.thumbX = this.baseX;
this.thumbY = this.baseY;
this.isActive = true;
});
canvas.addEventListener('touchmove', (e) => {
if(!this.isActive) return;
const touch = e.touches[0];
const dx = touch.clientX - this.baseX;
const dy = touch.clientY - this.baseY;
const distance = Math.sqrt(dx * dx + dy * dy);
if(distance < this.radius) {
this.thumbX = touch.clientX;
this.thumbY = touch.clientY;
} else {
const angle = Math.atan2(dy, dx);
this.thumbX = this.baseX + Math.cos(angle) * this.radius;
this.thumbY = this.baseY + Math.sin(angle) * this.radius;
}
});
canvas.addEventListener('touchend', () => {
this.isActive = false;
});
}
getDirection() {
if(!this.isActive) return { x: 0, y: 0 };
const dx = this.thumbX - this.baseX;
const dy = this.thumbY - this.baseY;
const distance = Math.sqrt(dx * dx + dy * dy);
return {
x: dx / distance,
y: dy / distance
};
}
draw(ctx) {
if(!this.isActive) return;
// 绘制底座
ctx.beginPath();
ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fill();
// 绘制摇杆
ctx.beginPath();
ctx.arc(this.thumbX, this.thumbY, this.radius * 0.4, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fill();
}
}
19. 游戏场景管理
19.1 场景切换系统
管理不同游戏场景:
javascript复制class SceneManager {
constructor() {
this.scenes = {};
this.currentScene = null;
}
addScene(name, scene) {
this.scenes[name] = scene;
}
switchTo(name) {
if(this.currentScene && this.currentScene.onExit) {
this.currentScene.onExit();
}
this.currentScene = this.scenes[name];
if(this.currentScene && this.currentScene.onEnter) {
this.currentScene.onEnter();
}
}
update() {
if(this.currentScene && this.currentScene.update) {
this.currentScene.update();
}
}
render(ctx) {
if(this.currentScene && this.currentScene.render) {
this.currentScene.render(ctx);
}
}
}
// 使用示例
const sceneManager = new SceneManager();
sceneManager.addScene('menu', new MenuScene());
sceneManager.addScene('game', new GameScene());
sceneManager.switchTo('menu');
19.2 视差滚动背景
创建深度感:
javascript复制class ParallaxBackground {
constructor(layers) {
this.layers = layers.map(layer => ({
image: layer.image,
speed: layer.speed,
x: 0,
y: 0,
width: layer.image.width,
height: layer.image.height
}));
}
update(cameraX) {
this.layers.forEach(layer => {
layer.x = -(cameraX * layer.speed) % layer.width;
});
}
render(ctx) {
this.layers.forEach(layer => {
// 绘制左半部分
ctx.drawImage(
layer.image,
layer.x, 0, layer.width - layer.x, layer.height,
0, 0, layer.width - layer.x, layer.height
);
// 绘制右半部分
if(layer.x > 0) {
ctx.drawImage(
layer.image,
0, 0, layer.x, layer.height,
layer.width - layer.x, 0, layer.x, layer.height
);
}
});
}
}
20. 游戏数据持久化
20.1 玩家成就系统
实现成就解锁:
javascript复制class AchievementSystem {
constructor() {
this.achievements = {};
this.unlocked = {};
}
addAchievement(id, name, description, condition) {
this.achievements[id] = {
name,
description,
condition
};
}
checkAchievements(gameState) {
Object.keys(this.achievements).forEach(id => {
if(!this.unlocked[id] && this.achievements[id].condition(gameState)) {
this.unlocked[id] = true;
this.showAchievement(id);
}
});
}
showAchievement(id) {
const ach = this.achievements[id];
console.log(`成就解锁: ${ach.name} - ${ach.description}`);
// 实际游戏中可以显示UI通知
}
}
20.2 游戏统计跟踪
记录玩家数据:
javascript复制class GameStats {
constructor() {
this.stats = {
playTime: 0,
enemiesDefeated: 0,
itemsCollected: 0,
deaths: 0
};
this.startTime = Date.now();
}
update() {
this.stats.playTime = Math.floor((Date.now() - this.startTime) / 1000);
}
increment(stat, amount = 1) {
if(this.stats[stat] !== undefined) {
this.stats[stat] += amount;
}
}
save() {
localStorage.setItem('gameStats', JSON.stringify(this.stats));
}
load() {
const saved = localStorage.getItem('gameStats');
if(saved) {
this.stats = JSON.parse(saved);
}
}
}
