1. 项目概述:用原生Web技术打造弹幕射击游戏
最近在整理过往项目时,翻出这个用纯HTML+CSS+JavaScript实现的弹幕射击游戏Demo。作为前端开发者入门游戏开发的经典案例,这种项目能帮助我们深入理解Canvas渲染、碰撞检测、游戏循环等核心概念。不同于使用现成游戏引擎,从零开始实现能让你真正掌握游戏开发的底层逻辑。
这个项目完整展示了如何用不到500行代码构建一个可玩的弹幕游戏,包含玩家控制、敌人生成、子弹系统、碰撞检测、分数统计等完整功能。特别适合已经掌握前端三件套基础,想尝试游戏开发的开发者练手。文末我会分享完整代码仓库,现在先带大家拆解其中的技术实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计
2.1 游戏主循环实现
游戏的核心是循环机制。我们采用经典的requestAnimationFrame实现:
javascript复制function gameLoop(timestamp) {
// 计算帧间隔时间
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// 更新游戏状态
update(deltaTime);
// 渲染画面
render();
// 继续循环
if (!gameOver) {
requestAnimationFrame(gameLoop);
}
}
这里有几个关键点:
- 使用deltaTime确保不同刷新率设备上游戏速度一致
- 将逻辑更新(update)和画面渲染(render)分离
- 通过gameOver标志控制循环终止
提示:避免在游戏循环中直接操作DOM,这会导致性能问题。所有渲染应在Canvas中完成。
2.2 实体-组件系统设计
虽然是小项目,但良好的架构能让代码更易维护:
javascript复制class Entity {
constructor() {
this.components = {};
}
addComponent(component) {
this.components[component.name] = component;
}
getComponent(name) {
return this.components[name];
}
}
// 示例组件
class SpriteComponent {
constructor(imageSrc) {
this.name = 'sprite';
this.image = new Image();
this.image.src = imageSrc;
}
}
这种设计让玩家、敌人、子弹等游戏对象可以灵活组合不同功能组件(渲染、物理、AI等)。
3. 关键技术实现细节
3.1 Canvas渲染优化
游戏采用Canvas 2D渲染,有几个性能优化点值得注意:
- 离屏Canvas:预渲染静态或重复元素
javascript复制const bufferCanvas = document.createElement('canvas');
const bufferCtx = bufferCanvas.getContext('2d');
// 预渲染背景
function renderBackground() {
bufferCtx.fillStyle = '#000';
bufferCtx.fillRect(0, 0, width, height);
// 绘制星空等静态元素...
}
- 分层渲染:将不同渲染层分开处理
javascript复制const layers = {
background: document.createElement('canvas'),
entities: document.createElement('canvas'),
ui: document.createElement('canvas')
};
// 每帧合成各层
function render() {
ctx.clearRect(0, 0, width, height);
ctx.drawImage(layers.background, 0, 0);
ctx.drawImage(layers.entities, 0, 0);
ctx.drawImage(layers.ui, 0, 0);
}
- 精灵图集:减少图片加载和切换开销
3.2 碰撞检测系统
弹幕游戏的核心挑战是高效的碰撞检测。我们采用空间分区优化:
javascript复制class CollisionSystem {
constructor(gridSize = 100) {
this.grid = {};
this.gridSize = gridSize;
}
addEntity(entity) {
const gridX = Math.floor(entity.x / this.gridSize);
const gridY = Math.floor(entity.y / this.gridSize);
const key = `${gridX},${gridY}`;
if (!this.grid[key]) {
this.grid[key] = [];
}
this.grid[key].push(entity);
}
checkCollisions() {
// 只检查相邻网格内的实体
// ...
}
}
对于圆形碰撞体,使用距离检测:
javascript复制function checkCollision(obj1, obj2) {
const dx = obj1.x - obj2.x;
const dy = obj1.y - obj2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < (obj1.radius + obj2.radius);
}
3.3 弹幕生成算法
实现了几种经典弹幕模式:
- 环形弹幕:
javascript复制function createCircleBullet(x, y, count, speed) {
const angleStep = (Math.PI * 2) / count;
for (let i = 0; i < count; i++) {
const angle = angleStep * i;
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
createBullet(x, y, vx, vy);
}
}
- 螺旋弹幕:
javascript复制let spiralAngle = 0;
function createSpiralBullet(x, y, count, speed) {
const angleStep = (Math.PI * 2) / count;
for (let i = 0; i < count; i++) {
const angle = angleStep * i + spiralAngle;
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
createBullet(x, y, vx, vy);
}
spiralAngle += 0.1;
}
- 追踪弹幕:
javascript复制function createHomingBullet(x, y, target, speed) {
const dx = target.x - x;
const dy = target.y - y;
const distance = Math.sqrt(dx * dx + dy * dy);
const vx = (dx / distance) * speed;
const vy = (dy / distance) * speed;
createBullet(x, y, vx, vy);
}
4. 性能优化实战经验
4.1 对象池技术
频繁创建销毁对象会导致GC卡顿,使用对象池重用对象:
javascript复制class BulletPool {
constructor() {
this.pool = [];
}
get(x, y, vx, vy) {
let bullet;
if (this.pool.length > 0) {
bullet = this.pool.pop();
bullet.reset(x, y, vx, vy);
} else {
bullet = new Bullet(x, y, vx, vy);
}
return bullet;
}
release(bullet) {
this.pool.push(bullet);
}
}
4.2 事件系统优化
避免直接在游戏循环中触发事件,使用批处理:
javascript复制class EventSystem {
constructor() {
this.queue = [];
}
emit(event, data) {
this.queue.push({ event, data });
}
process() {
while (this.queue.length) {
const { event, data } = this.queue.shift();
// 处理事件...
}
}
}
4.3 内存管理技巧
- 预加载所有游戏资源
- 及时销毁不再使用的对象
- 避免在游戏循环中创建新对象
- 使用TypedArray处理大量数值数据
5. 完整项目代码解析
项目目录结构:
code复制├── index.html # 主页面
├── css/
│ └── style.css # 基础样式
├── js/
│ ├── main.js # 游戏入口
│ ├── entity.js # 实体系统
│ ├── systems/ # 各子系统
│ │ ├── render.js
│ │ ├── collision.js
│ │ └── bullet.js
│ └── utils/ # 工具函数
│ ├── pool.js
│ └── math.js
└── assets/ # 资源文件
├── sprites/
└── sounds/
核心代码片段:
玩家控制实现:
javascript复制class PlayerController {
constructor(entity) {
this.entity = entity;
this.speed = 5;
this.keys = {
ArrowUp: false,
ArrowDown: false,
ArrowLeft: false,
ArrowRight: false
};
window.addEventListener('keydown', (e) => this.onKeyDown(e));
window.addEventListener('keyup', (e) => this.onKeyUp(e));
}
update() {
if (this.keys.ArrowUp) this.entity.y -= this.speed;
if (this.keys.ArrowDown) this.entity.y += this.speed;
if (this.keys.ArrowLeft) this.entity.x -= this.speed;
if (this.keys.ArrowRight) this.entity.x += this.speed;
// 边界检查
this.entity.x = Math.max(0, Math.min(this.entity.x, gameWidth));
this.entity.y = Math.max(0, Math.min(this.entity.y, gameHeight));
}
// 省略事件处理方法...
}
敌人生成系统:
javascript复制class EnemySpawner {
constructor() {
this.spawnTimer = 0;
this.spawnInterval = 2000; // 2秒
}
update(deltaTime) {
this.spawnTimer += deltaTime;
if (this.spawnTimer >= this.spawnInterval) {
this.spawnEnemy();
this.spawnTimer = 0;
// 难度递增
this.spawnInterval = Math.max(500, this.spawnInterval - 50);
}
}
spawnEnemy() {
const x = Math.random() * (gameWidth - 50);
const y = -50;
const enemy = new Entity();
enemy.addComponent(new SpriteComponent('assets/enemy.png'));
enemy.addComponent(new EnemyAI());
// ...
}
}
UI渲染系统:
javascript复制class UIRenderer {
constructor() {
this.score = 0;
this.lives = 3;
}
render(ctx) {
ctx.fillStyle = 'white';
ctx.font = '24px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${this.score}`, 20, 40);
ctx.textAlign = 'right';
ctx.fillText(`Lives: ${this.lives}`, gameWidth - 20, 40);
if (gameOver) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, gameWidth, gameHeight);
ctx.fillStyle = 'red';
ctx.font = '48px Arial';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', gameWidth / 2, gameHeight / 2);
}
}
}
6. 开发中的常见问题与解决方案
6.1 画面撕裂问题
症状:快速移动时出现水平撕裂线
解决方案:
javascript复制// 在requestAnimationFrame回调中重置变换
function render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
// 其他渲染代码...
}
6.2 移动设备触摸控制
添加触摸支持:
javascript复制const touchArea = document.getElementById('game-canvas');
let touchStartX = 0;
let touchStartY = 0;
touchArea.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
});
touchArea.addEventListener('touchmove', (e) => {
const touch = e.touches[0];
const dx = touch.clientX - touchStartX;
const dy = touch.clientY - touchStartY;
player.x += dx * 0.5;
player.y += dy * 0.5;
touchStartX = touch.clientX;
touchStartY = touch.clientY;
e.preventDefault();
});
6.3 音频播放限制
现代浏览器要求音频必须在用户交互后播放:
javascript复制let audioEnabled = false;
document.addEventListener('click', () => {
if (!audioEnabled) {
// 预加载所有音效
audioEnabled = true;
}
});
function playSound(src) {
if (!audioEnabled) return;
const audio = new Audio(src);
audio.volume = 0.3;
audio.play().catch(e => console.log('Audio play failed:', e));
}
7. 项目扩展方向
-
添加更多游戏机制:
- 能量系统
- 技能树
- BOSS战
-
增强视觉效果:
- 粒子特效
- 屏幕抖动
- 后处理效果
-
网络功能:
- 排行榜
- 多人协作模式
-
打包发布:
- 使用PWA技术做成可安装应用
- 打包为移动端应用(Cordova/ Capacitor)
完整项目代码已上传GitHub仓库(链接见文末)。这个项目虽然不大,但涵盖了游戏开发的核心概念。通过扩展和完善它,你可以深入理解游戏开发的各个方面。我在开发过程中最大的体会是:性能优化要早做,架构设计要灵活,测试要全面覆盖各种边界情况。
