求助:将原生JS贪吃蛇应用改造为React组件
哇,这情况太糟了——忙活一整天的代码没了还得从零开始,换成我肯定头大!不过把原生贪吃蛇改成React组件其实挺清晰的,咱们一步步来,既能用上React的state和props,还能把游戏逻辑理顺。
从零构建React版贪吃蛇组件
1. 基础组件结构与State设计
先搭好组件的骨架,用React函数组件(现在官方推荐的写法),把游戏核心状态都存在useState里,方便后续访问和修改:
import { useState, useEffect, useCallback } from 'react'; const SnakeGame = () => { // 蛇身:用数组存储每一段的坐标,初始是3节 const [snake, setSnake] = useState([{ x: 5, y: 5 }, { x: 4, y: 5 }, { x: 3, y: 5 }]); // 移动方向:初始向右 const [direction, setDirection] = useState('right'); // 食物位置 const [food, setFood] = useState({ x: 10, y: 10 }); // 游戏状态:running/paused/gameOver const [gameStatus, setGameStatus] = useState('paused'); // 分数 const [score, setScore] = useState(0); // 重置游戏函数 const resetGame = useCallback(() => { setSnake([{ x: 5, y: 5 }, { x: 4, y: 5 }, { x: 3, y: 5 }]); setDirection('right'); setScore(0); setGameStatus('paused'); generateFood(); }, []); // 渲染部分 return ( <div className="snake-game-container"> <div className="game-header"> <h2>贪吃蛇</h2> <p>当前分数: {score}</p> <button onClick={() => setGameStatus(prev => prev === 'running' ? 'paused' : 'running')}> {gameStatus === 'running' ? '暂停游戏' : '开始游戏'} </button> <button onClick={resetGame}>重置游戏</button> </div> <div className="game-board"> {/* 渲染蛇身 */} {snake.map((segment, idx) => ( <div key={idx} className="snake-segment" style={{ left: `${segment.x * 20}px`, top: `${segment.y * 20}px` }} /> ))} {/* 渲染食物 */} <div className="food" style={{ left: `${food.x * 20}px`, top: `${food.y * 20}px` }} /> </div> </div> ); }; export default SnakeGame;
2. 键盘事件处理(替代原生Keyboard命名空间)
原代码里的键盘映射逻辑,我们用React的useEffect监听全局键盘事件,同时用useCallback保证函数引用稳定:
const handleKeyPress = useCallback((e) => { // 对应原代码的Keyboard.Keymap const keyMap = { 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; const newDirection = keyMap[e.keyCode]; // 防止反向移动(比如向右时不能直接按左) const oppositeDirs = { left: 'right', right: 'left', up: 'down', down: 'up' }; if (newDirection && newDirection !== oppositeDirs[direction]) { setDirection(newDirection); } }, [direction]); // 监听键盘事件,组件卸载时清理 useEffect(() => { window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [handleKeyPress]);
3. 游戏核心逻辑(移动、碰撞、食物生成)
这部分是游戏的灵魂,我们用定时器实现游戏循环,处理蛇的移动、碰撞检测和食物生成:
// 生成随机食物(不能出现在蛇身上) const generateFood = useCallback(() => { setFood(() => { let newFood; do { newFood = { x: Math.floor(Math.random() * 20), // 假设棋盘是20x20格 y: Math.floor(Math.random() * 20) }; } while (snake.some(seg => seg.x === newFood.x && seg.y === newFood.y)); return newFood; }); }, [snake]); // 蛇的移动逻辑 const moveSnake = useCallback(() => { if (gameStatus !== 'running') return; setSnake(prevSnake => { const newSnake = [...prevSnake]; const head = { ...newSnake[0] }; // 根据方向更新头部位置 switch (direction) { case 'left': head.x -= 1; break; case 'right': head.x += 1; break; case 'up': head.y -= 1; break; case 'down': head.y += 1; break; default: break; } // 碰撞检测:撞墙或撞自己 const hitWall = head.x < 0 || head.x >= 20 || head.y < 0 || head.y >= 20; const hitSelf = newSnake.some(seg => seg.x === head.x && seg.y === head.y); if (hitWall || hitSelf) { setGameStatus('gameOver'); return prevSnake; } // 检查是否吃到食物 if (head.x === food.x && head.y === food.y) { setScore(prev => prev + 10); generateFood(); newSnake.unshift(head); // 吃到食物,蛇身变长(不删除尾部) } else { newSnake.unshift(head); newSnake.pop(); // 没吃到,正常移动(删除尾部) } return newSnake; }); }, [direction, food, gameStatus, generateFood]); // 游戏循环:每150ms移动一次 useEffect(() => { let gameInterval; if (gameStatus === 'running') { gameInterval = setInterval(moveSnake, 150); } return () => clearInterval(gameInterval); }, [gameStatus, moveSnake]);
4. 基础样式(让组件显示正常)
给组件加些简单的CSS,保证棋盘、蛇和食物能正确渲染:
.snake-game-container { display: flex; flex-direction: column; align-items: center; margin: 2rem; gap: 1rem; } .game-board { width: 400px; height: 400px; border: 2px solid #333; position: relative; background-color: #f5f5f5; } .snake-segment { position: absolute; width: 20px; height: 20px; background-color: #27ae60; border-radius: 2px; } .food { position: absolute; width: 20px; height: 20px; background-color: #e74c3c; border-radius: 50%; } .game-header button { margin: 0 0.5rem; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; background-color: #3498db; color: white; }
5. 用Props扩展组件灵活性
如果你想让组件更复用,可以通过props传递参数,比如棋盘大小、移动速度:
// 修改组件接收props const SnakeGame = ({ boardSize = 20, speed = 150 }) => { // 碰撞检测时用boardSize代替硬编码的20 const hitWall = head.x < 0 || head.x >= boardSize || head.y < 0 || head.y >= boardSize; // 生成食物时也用boardSize newFood = { x: Math.floor(Math.random() * boardSize), y: Math.floor(Math.random() * boardSize) }; // 游戏循环用传入的speed gameInterval = setInterval(moveSnake, speed); // 棋盘样式也动态计算 <div className="game-board" style={{ width: `${boardSize * 20}px`, height: `${boardSize * 20}px` }}> {/* ... */} </div> };
这样你就能根据需要调整游戏参数,完全符合React组件的复用特性。
内容的提问来源于stack exchange,提问作者Denis Angell
相关产品推荐
相关产品推荐

