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

React Canvas按右键无法移动方块问题求助

问题分析与解决方案

核心问题点

  • 闭包陷阱:animate、handleKeyDown、handleKeyUp等函数的依赖项不完整,导致它们始终引用组件初始渲染时的状态值,无法响应后续状态更新。
  • 状态直接修改:代码中存在velocity.y += gravity、position.y += velocity.y这类直接修改状态对象的操作,违反React状态不可变原则,会导致状态更新不被检测到。
  • 动画逻辑拆分混乱:animate和update函数中都在修改velocity状态,导致状态更新冲突,左右移动的逻辑被重力更新覆盖。
  • 键盘事件焦点问题:canvas的tabIndex=-1会导致难以获取焦点,无法触发键盘事件。

修正后的代码

import { useRef, useState, useCallback, useEffect } from 'react';

const gravity = 4.5;

const Canvas = () => {
  const { innerWidth: innerwidth, innerHeight: innerheight } = window;
  const canvasRef = useRef();

  const [position, setPosition] = useState({ x: 100, y: 100 });
  const [size, setSize] = useState({ width: 30, height: 30 });
  const [velocity, setVelocity] = useState({ x: 0, y: 0 });
  const [pressRight, setPressRight] = useState(false);
  const [pressLeft, setPressLeft] = useState(false);

  const draw = useCallback((context) => {
    context.fillStyle = 'red';
    context.fillRect(position.x, position.y, size.width, size.height);
  }, [position, size]); 

  const update = useCallback((canvas) => {
    // 处理左右移动速度
    let newXVelocity = 0;
    if (pressRight) newXVelocity = 5;
    if (pressLeft) newXVelocity = -5;

    // 处理重力与落地
    let newYVelocity = velocity.y;
    if (position.y + size.height < canvas.height) {
      newYVelocity += gravity;
    } else {
      newYVelocity = 0;
      // 落地时修正位置,防止方块超出画布
      setPosition(prev => ({ ...prev, y: canvas.height - size.height }));
      return;
    }

    // 更新位置(使用函数式更新获取最新状态)
    setPosition(prev => ({
      x: prev.x + newXVelocity,
      y: prev.y + newYVelocity
    }));
    setVelocity({ x: newXVelocity, y: newYVelocity });
  }, [position, size, velocity, pressRight, pressLeft]);

  const animate = useCallback((context, width, height, canvas) => {
    requestAnimationFrame(() => animate(context, width, height, canvas));
    context.clearRect(0, 0, width, height);
    update(canvas);
    draw(context);
  }, [update, draw]);

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    canvas.width = innerwidth - 10;
    canvas.height = innerheight - 10;
    // 让canvas获取焦点,确保键盘事件触发
    canvas.focus();
    animate(context, canvas.width, canvas.height, canvas);
  }, [animate]);

  const handleKeyDown = useCallback(({ key }) => {
    switch (key) {
      case 'ArrowLeft':
        setPressLeft(true);
        break;
      case 'ArrowRight':
        setPressRight(true);
        break;
      case ' ': // Spacebar
        setVelocity(prev => ({ ...prev, y: prev.y - 50 }));
        break;
      default:
        console.log(`Unknown key: ${key}`);
    }    
  }, []);

  const handleKeyUp = useCallback(({ key }) => {
    switch (key) {
      case 'ArrowLeft':
        setPressLeft(false);
        break;
      case 'ArrowRight':
        setPressRight(false);
        break;
      case ' ': 
        break;
      default:
        console.log(`Unknown key: ${key}`);
    }    
  }, []);

  return (
    <canvas
      ref={canvasRef}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onKeyUp={handleKeyUp}
      style={{ border: '1px solid #000' }} // 可选:添加边框方便识别canvas区域
    />
  );
};

export default Canvas;

关键修改说明

  • 修复闭包问题:给所有useCallback和useEffect添加完整的依赖项,确保函数能获取最新状态。
  • 状态不可变更新:使用函数式更新(setState(prev => ...))和新对象替换的方式修改状态,避免直接修改原对象。
  • 统一动画逻辑:将所有状态更新逻辑集中到update函数中,避免多函数修改状态导致的冲突。
  • 优化键盘事件:将tabIndex设为0,在useEffect中主动调用canvas.focus()确保能触发键盘事件,同时补全了左方向键的处理逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 17:40:53