You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端touchstart触发HTML Canvas卡顿问题求修复方案

修复iOS上Canvas游戏点击卡顿问题

问题描述

基于requestAnimationFrame实现的HTML Canvas小游戏,在iPhone的iOS系统中点击Canvas时会出现轻微卡顿、抖动或冻结现象,PC和安卓设备运行正常。已尝试在touchstart事件中调用preventDefault()与event.stopPropagation(),但问题未解决。

相关代码

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Game</title>
  <style>
    #gameCanvas {
      border: 1px solid #000;
    }
  </style>
</head>
<body>
<div class="game-container">
  <canvas id="gameCanvas"></canvas>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

JavaScript

const gameCanvas = document.getElementById('gameCanvas');
let context = gameCanvas.getContext('2d');
let requestId = null;
let lastTime = 0;
let pickupTimer = 0;
let obstacleTimer = 0;

const chopper = {
  x: 50,
  y: 150,
  width: 50,
  height: 20,
  dy: 0,
};

let pickups = new Set();
let obstacles = new Set();
const pickupWidth = 10;
const pickupHeight = 10;
const obstacleWidth = 30;
const obstacleHeight = 30;
let score = 0;
let gameRunning = true;
let gameSpeed = -0.5;
let isFlapping = false;

function spawnPickup(delta) {
  pickupTimer += delta;
  if (pickupTimer >= 0.5) { // 0.5秒间隔
    const x = context.canvas.width;
    const y = Math.random() * (context.canvas.height - pickupHeight);
    pickups.add({ x, y, width: pickupWidth, height: pickupHeight, value: 10, dx: gameSpeed });
    pickupTimer = 0;
  }
}

function spawnObstacle(delta) {
  obstacleTimer += delta;
  if(obstacleTimer >= 0.6) { // 0.6秒间隔
    const x = context.canvas.width;
    const y = Math.random() * (context.canvas.height - obstacleHeight);
    obstacles.add({ x, y, width: obstacleWidth, height: obstacleHeight, dx: gameSpeed});
    obstacleTimer = 0;
  }
}

function drawPickup(pickup) {
  context.fillStyle = 'gold';
  context.fillRect(pickup.x, pickup.y, pickup.width, pickup.height);
}

function drawObstacle(obstacle) {
  context.fillStyle = 'red';
  context.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height);
}

function drawChopper(deltaTime) {
  if (isFlapping) {
    chopper.dy = -300; // 调整为毫秒级速度
    isFlapping = false;
  }
  chopper.dy += 900 * deltaTime; // 重力调整为秒级加速度
  chopper.y += chopper.dy * deltaTime;

  if (chopper.y + chopper.height > context.canvas.height || chopper.y < 0) {
    endGame();
  }

  context.fillStyle = 'green';
  context.fillRect(chopper.x, chopper.y, chopper.width, chopper.height);
}

function checkCollision(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;
}

function handlePickupOverlap(deltaTime) {
  pickups.forEach(pickup => {
    pickup.x += pickup.dx * 1000 * deltaTime; // 转成毫秒级位移
    drawPickup(pickup);
    if (checkCollision(chopper, pickup)) {
      score += pickup.value;
      pickups.delete(pickup);
    } else if (pickup.x + pickup.width < 0) {
      pickups.delete(pickup);
    }
  });
}

function handleObstacleOverlap(deltaTime) {
  obstacles.forEach(obstacle => {
    obstacle.x += obstacle.dx * 1000 * deltaTime; // 转成毫秒级位移
    drawObstacle(obstacle);
    // if (checkCollision(chopper, obstacle)) {
    //   endGame();
    // } else if (obstacle.x + obstacle.width < 0) {
    //   obstacles.delete(obstacle);
    // }
  });
}

function updateGame(timestamp = 0) {
  const deltaTime = (timestamp - lastTime) / 1000; // 将毫秒转为秒
  lastTime = timestamp;

  if (gameRunning) {
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);

    drawChopper(deltaTime);
    spawnPickup(deltaTime)
    spawnObstacle(deltaTime)

    handlePickupOverlap(deltaTime);

    handleObstacleOverlap(deltaTime);

    requestId = requestAnimationFrame(updateGame);
  }
}

function endGame() {
  // gameRunning = false;

  // 测试用重置
  chopper.y = 140
  chopper.dy = 0;
}

function setupCanvas() {
  context.canvas.width = 300;
  context.canvas.height = 600;
  lastTime = performance.now(); // 初始化lastTime为当前时间
  requestId = requestAnimationFrame(updateGame);

  // 将触摸事件绑定到Canvas元素
  gameCanvas.addEventListener('touchstart', (event) => {
    event.preventDefault();
    event.stopPropagation();
    isFlapping = true;
  });

  // 阻止touchend的默认行为
  gameCanvas.addEventListener('touchend', (event) => {
    event.preventDefault();
    event.stopPropagation();
  });
}

setupCanvas();

修复方案

1. 修正deltaTime单位错误

原代码中deltaTime直接使用毫秒值进行计算,但逻辑里默认按秒处理,导致数值偏差过大,iOS的时间精度差异会放大这个问题。将deltaTime转为秒单位,同时调整所有速度、加速度参数为对应量级:

  • const deltaTime = (timestamp - lastTime) / 1000;
  • 调整直升机的速度、重力,以及障碍物/道具的移动速度为毫秒级或对应秒级参数

2. 优化触摸事件绑定

  • 将触摸事件绑定到Canvas元素而非window,减少事件冒泡的层级和干扰
  • 添加touchend事件的默认行为阻止,避免iOS浏览器触发后续的滚动、缩放等操作

3. 启用Canvas硬件加速

在CSS中给Canvas添加以下属性,强制iOS启用硬件加速,提升渲染性能:

#gameCanvas {
  transform: translateZ(0);
  backface-visibility: hidden;
  will-change: transform;
}

4. 初始化lastTime值

在setupCanvas中用performance.now()初始化lastTime,避免第一帧的deltaTime异常过大导致的跳变。

内容的提问来源于stack exchange,提问作者watupbrah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:47:32