React中用setInterval绘制Canvas图形的施法速度更新异常问题
问题:React + Canvas游戏中施法速度滑块修改后不立即生效
我用HTML Canvas和React做了一款对战小游戏:两名英雄(大圆形)在场地两侧移动,互相投掷法术(小圆形)。每个英雄有调节移动速度和施法速度的滑块,目前移动速度调节正常,但施法速度修改后不会立即生效,只有鼠标移到Canvas区域后才变化。
问题原因分析
- 直接修改外部数据对象,React未追踪变化:直接修改从
data导入的hero1和hero2对象属性,这些不是React状态,React无法感知到变化,也就不会触发useInterval的更新。 - useInterval缺少依赖项:
useInterval的延迟参数hero1.castSpeed/hero2.castSpeed变化时,组件没有重新执行useInterval来更新定时器。 - rangeInputHandler的逻辑错误:修改hero2施法速度时,错误地赋值给了
hero1.castSpeed,导致hero2的施法速度永远不会变化。 - SpellCast.js的addSpell逻辑问题:每次调用
addSpell都会清空hero.spellPool,导致英雄永远只能有一个法术在场上。
修复步骤
1. 将英雄数据转为React状态
把hero1和hero2从导入的静态对象转为React的useState状态,这样修改属性时会触发组件重渲染:
// 替换原来的const { hero1, hero2, spell } = data; const [hero1, setHero1] = useState({...data.hero1}); const [hero2, setHero2] = useState({...data.hero2}); const { spell } = data;
2. 修复rangeInputHandler的更新逻辑
使用状态更新函数来修改英雄属性,确保状态正确更新,同时修正hero2施法速度的赋值错误:
function rangeInputHandler(event) { event.preventDefault(); const { id, value } = event.target; if (id === "hero1_move-speed_control") { setHero1(prev => ({...prev, moveSpeed: Number(value)})); } if (id === "hero2_move-speed_control") { setHero2(prev => ({...prev, moveSpeed: Number(value)})); } if (id === "hero1_cast-speed_control") { setHero1(prev => ({...prev, castSpeed: Number(value) * 1000})); } if (id === "hero2_cast-speed_control") { // 修复:这里应该修改hero2的castSpeed setHero2(prev => ({...prev, castSpeed: Number(value) * 1000})); } }
3. 给useInterval添加依赖项
让useInterval依赖对应的castSpeed,这样当施法速度变化时,定时器会重新创建:
useInterval(() => { addSpell(hero1, spell); }, hero1.castSpeed); useInterval(() => { addSpell(hero2, spell); }, hero2.castSpeed);
(注:确保你的useInterval自定义hook支持依赖变化时重新启动定时器,标准实现会处理这种情况)
4. 修复SpellCast.js中的addSpell逻辑
不要每次添加法术都清空spellPool,改为向数组中push新法术:
export function addSpell(hero, spell) { // 移除清空数组的代码,直接push新法术 hero.spellPool.push( new Spell( hero.x, hero.y, spell.dx, spell.dy, spell.rad, hero.spellColor, hero.id ) ); } // 同时记得在初始化英雄数据时,给spellPool设置为空数组 // 比如在data.js里给hero1和hero2加上spellPool: []
5. 调整Canvas渲染的依赖
确保Canvas的渲染函数能获取到最新的英雄状态,把hero1和hero2加入useEffect的依赖数组:
useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); const render = () => { context.clearRect(0, 0, canvas.width, canvas.height); HeroMove(context, hero1); HeroMove(context, hero2); hero1.spellPool.forEach((spell) => { spell.cast(context); }); hero2.spellPool.forEach((spell) => { spell.cast(context); }); collisionBorder(canvas, hero1, hero2); requestAnimationFrame(render); }; const animationId = requestAnimationFrame(render); // 添加清理函数,避免内存泄漏 return () => cancelAnimationFrame(animationId); }, [hero1, hero2]); // 加入依赖,状态变化时重新渲染
额外优化提示
- 鼠标移动事件中,
setMouseCoordinates等状态每次都创建新数组,会导致不必要的重渲染,可以考虑只存储最新坐标而不是历史数组:setMouseCoordinates(currentMouseCoord) collisionBorder、HeroMove等函数如果直接修改英雄状态,建议改为返回新状态,保持状态的不可变性
内容的提问来源于stack exchange,提问作者Tim M.
相关产品推荐
相关产品推荐

