HTML5 Canvas小游戏开发的核心算法

文章目录

HTML5 Canvas小游戏开发的核心算法

在HTML5 Canvas开发小游戏时,算法是实现核心玩法(如碰撞交互、AI行为、物理效果、动画过渡)的基础。以下是最常用的6类核心算法,包含原理、代码示例、使用场景、注意事项及总结。

一、碰撞检测算法(Collision Detection)

用途:判断游戏中两个物体(如角色与敌人、子弹与障碍物)是否接触,是交互逻辑的核心。
常见场景:角色受伤、道具拾取、边界限制。

1. 轴对齐矩形碰撞(Axis-Aligned Bounding Box, AABB)

原理:两个矩形的边界完全分离则不碰撞,否则碰撞(适合方块、UI元素等规则形状)。
判断条件

  • 矩形A的右边界 ≤ 矩形B的左边界 → 不碰撞
  • 矩形A的左边界 ≥ 矩形B的右边界 → 不碰撞
  • 矩形A的下边界 ≤ 矩形B的上边界 → 不碰撞
  • 矩形A的上边界 ≥ 矩形B的下边界 → 不碰撞
  • 以上均不满足 → 碰撞
    在这里插入图片描述

代码示例

// 定义矩形:{x: 左上角x, y: 左上角y, width: 宽, height: 高}
function checkAABBCollision(rectA, rectB) {
  return (
    rectA.x < rectB.x + rectB.width &&  // A左 < B右
    rectA.x + rectA.width > rectB.x &&  // A右 > B左
    rectA.y < rectB.y + rectB.height && // A上 < B下
    rectA.y + rectA.height > rectB.y    // A下 > B上
  );
}

// 测试
const player = { x: 100, y: 100, width: 50, height: 50 };
const enemy = { x: 130, y: 130, width: 50, height: 50 };
console.log(checkAABBCollision(player, enemy)); // true(碰撞)

2. 圆形碰撞检测

原理:两个圆形的圆心距离 ≤ 半径之和则碰撞(适合角色、子弹等圆形/近似圆形物体)。
判断条件

  • 计算两圆心距离的平方(避免开方运算,提升性能)
  • 若距离平方 ≤ (半径A + 半径B)的平方 → 碰撞
    在这里插入图片描述

代码示例

// 定义圆形:{x: 圆心x, y: 圆心y, radius: 半径}
function checkCircleCollision(circleA, circleB) {
  const dx = circleA.x - circleB.x; // x方向距离
  const dy = circleA.y - circleB.y; // y方向距离
  const distanceSquared = dx * dx + dy * dy; // 距离平方(避免Math.sqrt)
  const radiusSum = circleA.radius + circleB.radius;
  return distanceSquared <= radiusSum * radiusSum;
}

// 测试
const bullet = { x: 200, y: 200, radius: 10 };
const enemy = { x: 205, y: 205, radius: 15 };
console.log(checkCircleCollision(bullet, enemy)); // true(碰撞)

3. 分离轴定理(Separating Axis Theorem, SAT)

原理:判断任意凸多边形是否碰撞(通过检查是否存在一条轴,使两多边形在该轴上的投影不重叠)。适合三角形、六边形等不规则凸多边形(如游戏中的地形、复杂角色碰撞箱)。
在这里插入图片描述

代码示例(简化版,检测两个三角形碰撞):

// 计算多边形在轴上的投影(最小值和最大值)
function getProjection(polygon, axis) {
  let min = dotProduct(polygon[0], axis);
  let max = min;
  for (const point of polygon) {
    const p = dotProduct(point, axis);
    min = Math.min(min, p);
    max = Math.max(max, p);
  }
  return { min, max };
}

// 点积计算(用于投影)
function dotProduct(a, b) {
  return a.x * b.x + a.y * b.y;
}

// 检查两个投影是否重叠
function projectionsOverlap(p1, p2) {
  return !(p1.max < p2.min || p2.max < p1.min);
}

// SAT算法主函数(polygonA和polygonB为点数组,如[{x,y}, {x,y}, ...])
function checkSATCollision(polygonA, polygonB) {
  // 生成所有可能的分离轴(每个边的垂直向量)
  const axes = [];
  // 多边形A的边
  for (let i = 0; i < polygonA.length; i++) {
    const p1 = polygonA[i];
    const p2 = polygonA[(i + 1) % polygonA.length];
    const edge = { x: p2.x - p1.x, y: p2.y - p1.y };
    // 垂直于边的轴(法向量)
    const axis = { x: -edge.y, y: edge.x };
    axes.push(axis);
  }
  // 多边形B的边
  for (let i = 0; i < polygonB.length; i++) {
    const p1 = polygonB[i];
    const p2 = polygonB[(i + 1) % polygonB.length];
    const edge = { x: p2.x - p1.x, y: p2.y - p1.y };
    const axis = { x: -edge.y, y: edge.x };
    axes.push(axis);
  }

  // 检查所有轴的投影是否重叠
  for (const axis of axes) {
    const projA = getProjection(polygonA, axis);
    const projB = getProjection(polygonB, axis);
    if (!projectionsOverlap(projA, projB)) {
      return false; // 找到分离轴,不碰撞
    }
  }
  return true; // 所有轴投影都重叠,碰撞
}

// 测试:两个三角形
const triangleA = [{x:10,y:10}, {x:30,y:10}, {x:20,y:30}];
const triangleB = [{x:25,y:25}, {x:45,y:25}, {x:35,y:45}];
console.log(checkSATCollision(triangleA, triangleB)); // true(碰撞)

注意事项

  • 优先使用AABB或圆形碰撞(性能最优),复杂形状才用SAT(计算成本高);
  • 大场景中可先通过AABB做“粗检测”,再用SAT做“精检测”(减少计算量);
  • 非凸多边形需拆分为凸多边形后再用SAT检测。

二、路径寻找算法(Pathfinding)

用途:让AI角色(如敌人、NPC)自动找到从起点到终点的最优路径(避开障碍物)。
常见场景:策略游戏中单位移动、RPG中敌人追击玩家。

A* 算法(A-Star)

原理:基于“代价估计”的网格路径搜索,通过f(n) = g(n) + h(n)评估节点优先级:

  • g(n):从起点到当前节点的实际代价(已走步数);
  • h(n):从当前节点到终点的估计代价(启发函数,如曼哈顿距离、欧氏距离);
  • 优先选择f(n)最小的节点,最终找到最优路径。

代码示例(网格地图寻路):

// 网格节点类
class Node {
  constructor(x, y, isObstacle = false) {
    this.x = x; // 网格x坐标
    this.y = y; // 网格y坐标
    this.isObstacle = isObstacle; // 是否为障碍物
    this.g = 0; // 起点到当前节点的代价
    this.h = 0; // 当前节点到终点的估计代价
    this.f = 0; // f = g + h
    this.parent = null; // 父节点(用于回溯路径)
  }
}

// 生成网格地图(rows行,cols列,随机障碍物)
function createGrid(rows, cols) {
  const grid = [];
  for (let y = 0; y < rows; y++) {
    const row = [];
    for (let x = 0; x < cols; x++) {
      // 10%概率为障碍物
      const isObstacle = Math.random() < 0.1;
      row.push(new Node(x, y, isObstacle));
    }
    grid.push(row);
  }
  return grid;
}

// 启发函数:曼哈顿距离(适合网格,只允许上下左右移动)
function manhattanDistance(node, endNode) {
  return Math.abs(node.x - endNode.x) + Math.abs(node.y - endNode.y);
}

// A* 算法主函数
function aStarSearch(grid, startNode, endNode) {
  const openList = [startNode]; // 待检查节点
  const closedList = []; // 已检查节点

  while (openList.length > 0) {
    // 1. 从openList中找f值最小的节点
    let currentIndex = 0;
    for (let i = 0; i < openList.length; i++) {
      if (openList[i].f < openList[currentIndex].f) {
        currentIndex = i;
      }
    }
    const currentNode = openList.splice(currentIndex, 1)[0];
    closedList.push(currentNode);

    // 2. 找到终点,回溯路径
    if (currentNode === endNode) {
      const path = [];
      let temp = currentNode;
      while (temp) {
        path.push({ x: temp.x, y: temp.y });
        temp = temp.parent;
      }
      return path.reverse(); // 反转路径(从起点到终点)
    }

    // 3. 生成相邻节点(上下左右)
    const neighbors = [];
    const directions = [
      { x: 0, y: -1 }, // 上
      { x: 0, y: 1 },  // 下
      { x: -1, y: 0 }, // 左
      { x: 1, y: 0 }   // 右
    ];
    for (const dir of directions) {
      const x = currentNode.x + dir.x;
      const y = currentNode.y + dir.y;
      // 检查是否在网格内且非障碍物
      if (
        x >= 0 && x < grid[0].length &&
        y >= 0 && y < grid.length &&
        !grid[y][x].isObstacle
      ) {
        neighbors.push(grid[y][x]);
      }
    }

    // 4. 处理相邻节点
    for (const neighbor of neighbors) {
      if (closedList.includes(neighbor)) continue; // 已检查过,跳过

      // 计算临时g值(当前节点g + 1步代价)
      const tempG = currentNode.g + 1;
      let isNewPath = false;

      if (openList.includes(neighbor)) {
        // 已在待检查列表,判断新路径是否更优
        if (tempG < neighbor.g) {
          neighbor.g = tempG;
          isNewPath = true;
        }
      } else {
        // 不在待检查列表,加入并初始化
        neighbor.g = tempG;
        openList.push(neighbor);
        isNewPath = true;
      }

      // 若新路径更优,更新h、f和父节点
      if (isNewPath) {
        neighbor.h = manhattanDistance(neighbor, endNode);
        neighbor.f = neighbor.g + neighbor.h;
        neighbor.parent = currentNode;
      }
    }
  }

  // 没有找到路径
  return null;
}

// 测试
const grid = createGrid(10, 10); // 10x10网格
const start = grid[1][1];
const end = grid[8][8];
const path = aStarSearch(grid, start, end);
console.log('路径:', path); // 输出从(1,1)到(8,8)的坐标数组

注意事项

  • 启发函数需“可接受”(h(n) ≤ 实际代价),否则可能找不到最优路径;
  • 大网格(如100x100)需优化:用哈希表存储openListclosedList(替代数组,提升查找速度);
  • 允许斜向移动时,启发函数改用欧氏距离(Math.hypot(dx, dy)),且g(n)增加√2代价。

三、物理模拟算法(Physics Simulation)

用途:模拟现实世界的物理规律(如重力、加速度、碰撞反弹),让游戏物体运动更自然。
常见场景:平台跳跃游戏(重力下落)、弹球类游戏(碰撞反弹)。

1. 基础运动模拟(速度与加速度)

原理:通过“时间步长”更新物体位置:

  • 速度 = 速度 + 加速度 × 时间(v = v0 + a × dt);
  • 位置 = 位置 + 速度 × 时间(s = s0 + v × dt)。

代码示例(重力下落模拟):

// 游戏物体
const ball = {
  x: 100,    // x坐标
  y: 50,     // y坐标
  vx: 2,     // x方向速度
  vy: 0,     // y方向速度
  ax: 0,     // x方向加速度(此处为0)
  ay: 0.5,   // y方向加速度(重力)
  radius: 10
};

// Canvas渲染
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// 游戏循环(每帧更新)
function gameLoop(timestamp) {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 1. 更新物理状态(时间步长dt取16ms,约60帧/秒)
  const dt = 16;
  ball.vy += ball.ay * dt; // 更新y方向速度(重力加速)
  ball.x += ball.vx * dt;  // 更新x位置
  ball.y += ball.vy * dt;  // 更新y位置

  // 2. 边界碰撞(地面反弹)
  if (ball.y + ball.radius > canvas.height) {
    ball.y = canvas.height - ball.radius; // 防止穿出边界
    ball.vy = -ball.vy * 0.8; // 反弹(0.8为弹性系数,能量损耗)
  }

  // 3. 渲染球
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
  ctx.fillStyle = 'red';
  ctx.fill();

  requestAnimationFrame(gameLoop);
}

// 启动游戏循环
requestAnimationFrame(gameLoop);

2. 弹性碰撞模拟(动量守恒)

原理:两物体碰撞后,速度根据动量守恒和能量守恒更新(简化版:只考虑一维碰撞)。

代码示例(两个小球碰撞):

// 弹性碰撞公式(一维)
function resolveCollision(ballA, ballB) {
  // 计算碰撞前的速度差
  const vDiff = ballA.vx - ballB.vx;
  // 计算碰撞后的速度(假设质量相同)
  ballA.vx = ballB.vx;
  ballB.vx = ballA.vx + vDiff;
}

// 结合圆形碰撞检测和运动模拟,在碰撞时调用resolveCollision

注意事项

  • 时间步长(dt)需固定(如用requestAnimationFrame的时间戳计算实际间隔),否则运动速度会随帧率波动;
  • 复杂物理(如旋转、摩擦力)可引入向量库(如gl-matrix),避免重复开发;
  • 高性能需求(如1000+物体)需简化物理模型(如忽略小物体碰撞)。

四、动画插值算法(Animation Interpolation)

用途:实现物体平滑移动、缩放、旋转(如角色移动、UI过渡),避免“跳变”感。

线性插值(Linear Interpolation, Lerp)

原理:在两个值之间按比例计算中间值,公式:lerp(a, b, t) = a + (b - a) × tt∈[0,1],0返回a,1返回b)。

代码示例(物体平滑移动到目标点):

// 线性插值函数
function lerp(a, b, t) {
  return a + (b - a) * t;
}

// 游戏物体
const box = {
  x: 100,    // 当前x
  y: 100,    // 当前y
  targetX: 300, // 目标x
  targetY: 200  // 目标y
};

// 渲染与更新
function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 用lerp平滑移动(t=0.1,每次靠近目标10%)
  box.x = lerp(box.x, box.targetX, 0.1);
  box.y = lerp(box.y, box.targetY, 0.1);

  // 渲染矩形
  ctx.fillRect(box.x, box.y, 50, 50);
  requestAnimationFrame(update);
}

update();

缓动函数(Easing Functions)

原理:在Lerp基础上加入非线性变换(如先快后慢、弹跳),让动画更自然。常见缓动类型:easeOutQuad(二次方减速)、easeInOutCubic(三次方先加速后减速)。

代码示例(easeOutQuad缓动):

// easeOutQuad:t²减速(t∈[0,1])
function easeOutQuad(t) {
  return 1 - (1 - t) * (1 - t);
}

// 带缓动的移动
function updateWithEasing() {
  // 计算当前进度(0到1)
  const progress = Math.min(1, (box.x - 100) / (300 - 100)); // 假设从100到300
  const t = easeOutQuad(progress); // 应用缓动

  box.x = lerp(100, 300, t);
  // ... 渲染
}

注意事项

  • Lerp的t值过大会导致“overshoot”(超过目标),需根据场景调整(如t=0.1适合平滑跟随);
  • 缓动函数可通过工具生成(如Easing Functions Cheat Sheet),避免重复编写。

五、粒子系统算法(Particle System)

用途:模拟大量微小物体的运动(如爆炸特效、烟雾、雨滴),增强游戏视觉效果。

核心逻辑

  1. 粒子创建:初始化粒子的位置、速度、生命周期、大小、颜色;
  2. 粒子更新:每帧更新位置(受重力/风力影响)、减少生命周期、更新透明度/大小;
  3. 粒子渲染:绘制所有存活粒子;
  4. 粒子回收:移除生命周期结束的粒子(避免内存泄漏)。

代码示例(爆炸特效):

class Particle {
  constructor(x, y) {
    this.x = x;         // 初始位置(爆炸中心)
    this.y = y;
    this.life = 1;      // 生命周期(1为满,0为消失)
    this.lifeLoss = 0.02 + Math.random() * 0.03; // 每帧减少的生命周期
    // 随机速度(向四周扩散)
    const angle = Math.random() * Math.PI * 2;
    const speed = 1 + Math.random() * 3;
    this.vx = Math.cos(angle) * speed;
    this.vy = Math.sin(angle) * speed;
    this.size = 2 + Math.random() * 3; // 随机大小
    this.color = `hsl(${Math.random() * 60}, 100%, 50%)`; // 橙色系
  }

  update() {
    // 更新位置
    this.x += this.vx;
    this.y += this.vy;
    // 加入重力(y方向加速)
    this.vy += 0.05;
    // 减少生命周期
    this.life -= this.lifeLoss;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.globalAlpha = this.life; // 随生命周期降低透明度
    ctx.fill();
    ctx.globalAlpha = 1; // 重置透明度
  }
}

// 粒子系统管理
class ParticleSystem {
  constructor() {
    this.particles = [];
  }

  // 发射粒子(x,y为中心,count为数量)
  emit(x, y, count) {
    for (let i = 0; i < count; i++) {
      this.particles.push(new Particle(x, y));
    }
  }

  updateAndDraw(ctx) {
    // 过滤存活粒子(life > 0)
    this.particles = this.particles.filter(p => p.life > 0);
    // 更新并绘制所有粒子
    for (const p of this.particles) {
      p.update();
      p.draw(ctx);
    }
  }
}

// 使用示例
const particleSystem = new ParticleSystem();
// 点击画布时发射粒子
canvas.addEventListener('click', (e) => {
  particleSystem.emit(e.offsetX, e.offsetY, 50); // 每次点击发射50个粒子
});

// 游戏循环中更新
function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  particleSystem.updateAndDraw(ctx);
  requestAnimationFrame(loop);
}
loop();

注意事项

  • 粒子数量过大会导致性能下降(建议单系统不超过500个粒子);
  • 用对象池(Object Pool)复用粒子(避免频繁创建/删除对象),提升性能;
  • 优化渲染:用CanvasRenderingContext2DfillRect替代arc(绘制矩形粒子更快)。

六、随机生成算法(Random Generation)

用途:随机生成地图、道具位置、敌人波次等,增加游戏可玩性。

1. 基础随机与种子随机

原理:用Math.random()生成随机数,但需注意:

  • 普通随机每次运行结果不同;
  • 种子随机(如seedrandom库)可固定随机序列(便于复现bug或生成相同地图)。

代码示例(随机生成道具位置):

// 生成不重叠的随机道具位置
function generateItems(count, mapWidth, mapHeight, itemSize) {
  const items = [];
  while (items.length < count) {
    // 随机位置(避免超出地图)
    const x = Math.random() * (mapWidth - itemSize);
    const y = Math.random() * (mapHeight - itemSize);
    const newItem = { x, y, size: itemSize };
    // 检查是否与已有道具重叠
    const isOverlap = items.some(item => 
      checkAABBCollision(newItem, item)
    );
    if (!isOverlap) {
      items.push(newItem);
    }
  }
  return items;
}

// 生成5个道具(地图500x500,道具大小20)
const items = generateItems(5, 500, 500, 20);

2. Perlin噪声(用于自然地形生成)

原理:生成平滑的随机值(避免纯随机的“锯齿感”),适合模拟地形高度、云层密度等。

代码示例(用Perlin噪声生成地形):

// 简化版Perlin噪声(完整实现需参考算法细节)
function perlinNoise(x) {
  // 实际项目中建议使用成熟库(如noisejs)
  const x0 = Math.floor(x);
  const x1 = x0 + 1;
  const t = x - x0;
  const tSmooth = t * t * (3 - 2 * t); // 平滑插值
  return lerp(
    Math.sin(x0) * 0.5 + 0.5, // 随机值1
    Math.sin(x1) * 0.5 + 0.5, // 随机值2
    tSmooth
  );
}

// 生成地形
function drawTerrain() {
  ctx.beginPath();
  ctx.moveTo(0, canvas.height);
  for (let x = 0; x < canvas.width; x++) {
    // 用Perlin噪声计算y坐标(地形高度)
    const noise = perlinNoise(x * 0.02); // 缩放噪声频率
    const y = canvas.height - noise * 100; // 地形高度范围0-100
    ctx.lineTo(x, y);
  }
  ctx.lineTo(canvas.width, canvas.height);
  ctx.fillStyle = 'green';
  ctx.fill();
}

drawTerrain();

注意事项

  • 避免在循环中频繁调用Math.random()(性能差),可预生成随机数数组;
  • 随机生成需控制“随机性”(如道具位置不能太密集,地形高度需在合理范围)。

总结

HTML5 Canvas小游戏开发的核心算法围绕“交互、运动、视觉、随机性”四大维度,选择算法时需平衡“效果”与“性能”:

  1. 优先级:碰撞检测(AABB/圆形)→ 动画插值(Lerp)→ 物理模拟 → 路径寻找 → 粒子系统 → 随机生成(按需使用);
  2. 性能优化:复杂算法(如SAT、A*)需结合“粗检测+精检测”,粒子系统用对象池,避免频繁DOM操作;
  3. 学习建议:先掌握基础算法(碰撞、Lerp),再逐步深入复杂算法(A*、Perlin噪声),结合实际项目(如开发一个简单的弹球游戏)练习。

这些算法是游戏逻辑的“骨架”,灵活组合可实现多样玩法,而优化细节(如减少计算量、复用资源)则决定了游戏的流畅度。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值