React Canvas精灵动画瞬间播放后消失问题求助
问题分析与解决方案
核心问题
你的动画快速播放后消失,根源在两个关键问题:
- 精灵图重复加载+异步绘制:每次调用
SpriteAni都新建Image实例并设置src,图片加载是异步的。前几次drawImage执行时图片还没加载完成,根本没画出内容;等图片加载好时,currentFrame已经被requestAnimationFrame的60fps循环快速推进到超出精灵图的帧范围,导致后续绘制空白。 - 帧更新无节流:即使删除手动更新
currentFrame的代码,requestAnimationFrame每秒跑60次,加上图片加载的延迟,等图片能绘制时,状态或绘制逻辑的累积变化依然会导致帧快速跳过。
修复步骤
1. 缓存精灵图,只加载一次
把精灵图的加载逻辑从SpriteAni中抽离,放在组件内缓存,避免重复加载:
// 在Canvas组件内添加图片缓存逻辑 const spriteCache = useRef({}); const loadSprite = (animName) => { if (!spriteCache.current[animName]) { const img = new Image(); img.src = `./images/char_${animName}.png`; spriteCache.current[animName] = img; } return spriteCache.current[animName]; };
修改SpriteAni,接收已加载的图片,不再内部创建,同时添加加载判断:
export default function SpriteAni(ctx, charSprite, currentFrame) { const posx = 320; const posy = 200; const sheet_width = 32; const sheet_height = 40; const sprite_width = sheet_width * 1.5; const sprite_height = sheet_height * 1.5; // 图片加载完成后再绘制 if (charSprite.complete) { ctx.drawImage(charSprite, currentFrame * sheet_width, 0, sheet_width, sheet_height, posx, posy, sprite_width * 1.5, sprite_height * 1.5); } }
2. 控制帧更新速度
requestAnimationFrame是60fps,直接每帧更新currentFrame会导致动画过快。添加节流逻辑,比如每秒播放10帧:
// 在Canvas组件内添加节流相关的ref const lastFrameUpdateTime = useRef(Date.now()); const frameRate = 10; // 每秒10帧 const frameInterval = 1000 / frameRate; const drawFrame = () => { const ctx = canvasRef.current.getContext('2d'); ctx.clearRect(0,0,constants.CANVAS_WIDTH,constants.CANVAS_HEIGHT); handleBackground(ctx); const charSprite = loadSprite(charAnim); SpriteAni(ctx, charSprite, currentFrame); // 达到间隔时间再更新帧 const now = Date.now(); if (now - lastFrameUpdateTime.current >= frameInterval) { setCurrentFrame(prev => { if (prev >= walk_frames) return 0; return prev + 1; }); lastFrameUpdateTime.current = now; } }
3. 修正组件清理与依赖逻辑
确保useEffect的依赖数组正确,同时保证动画循环的清理逻辑可靠:
useEffect(() => { const tick = () => { if (!canvasRef.current) return; drawFrame(); requestIdRef.current = requestAnimationFrame(tick); }; requestIdRef.current = requestAnimationFrame(tick); return () => { cancelAnimationFrame(requestIdRef.current); }; }, [charAnim])
最终修改后的Canvas组件片段
import { useEffect, useRef, useState } from "react"; import SpriteAni from '../SpriteAni.jsx'; const Canvas = ({ width, height }) => { const canvasRef = useRef(null); const requestIdRef = useRef(null); const spriteCache = useRef({}); const lastFrameUpdateTime = useRef(Date.now()); const frameRate = 10; const frameInterval = 1000 / frameRate; const [currentFrame, setCurrentFrame] = useState(0); const [charAnim, setCharAnim] = useState('walk') const walk_frames = 9; const loadSprite = (animName) => { if (!spriteCache.current[animName]) { const img = new Image(); img.src = `./images/char_${animName}.png`; spriteCache.current[animName] = img; } return spriteCache.current[animName]; }; const drawFrame = () => { const ctx = canvasRef.current.getContext('2d'); ctx.clearRect(0,0,constants.CANVAS_WIDTH,constants.CANVAS_HEIGHT); handleBackground(ctx); const charSprite = loadSprite(charAnim); SpriteAni(ctx, charSprite, currentFrame); const now = Date.now(); if (now - lastFrameUpdateTime.current >= frameInterval) { setCurrentFrame(prev => prev >= walk_frames ? 0 : prev + 1); lastFrameUpdateTime.current = now; } } useEffect(() => { const tick = () => { if (!canvasRef.current) return; drawFrame(); requestIdRef.current = requestAnimationFrame(tick); }; requestIdRef.current = requestAnimationFrame(tick); return () => { cancelAnimationFrame(requestIdRef.current); }; }, [charAnim]) return ( <canvas width={width} height={height} style={canvasStyle} ref={canvasRef} /> ) } export default Canvas const canvasStyle = { border: "1px solid black", position: "absolute", }
内容的提问来源于stack exchange,提问作者CakeoftheCup
相关产品推荐
相关产品推荐

