项目概述
本项目是一个基于 Three.js 和 React 的双人叠箱子游戏,支持通过 Socket.io 进行实时对战。在开发过程中,我们对代码进行了大规模重构,将游戏逻辑从主页面组件中抽离出来,形成了独立可复用的游戏组件。
重构背景
原始代码问题
代码冗余:主游戏页面中包含了大量的 Three.js 游戏逻辑代码
ref 引用过多:存在大量的 useRef 引用,导致代码维护困难
Socket 连接管理混乱:每次状态更新都会创建新的 Socket 连接
组件职责不清:UI 逻辑和游戏逻辑混杂在一起
难以复用:游戏逻辑无法在其他地方复用
重构目标
分离关注点:将游戏逻辑和 UI 逻辑分离
提高复用性:创建独立的游戏组件
简化状态管理:优化 Socket 连接和游戏状态管理
提升可维护性:减少代码重复,提高代码清晰度
重构过程
阶段一:游戏逻辑抽离
1. 创建独立的游戏组件
文件:src/components/StackGame.js
// 抽离出的核心类
class Base {
// Three.js 基础功能
}
class Stack extends Base {
// 游戏逻辑
}
// React 组件包装
const StackGame = forwardRef((props, ref) => {
// 组件逻辑
});主要改进:
将 Three.js 相关的所有逻辑封装到独立的类中
使用 React forwardRef 暴露游戏控制方法
通过回调函数处理游戏状态变化
2. 重构主游戏页面
文件:src/routes/Game/index.js
重构前:
// 大量的游戏逻辑代码
class Base { /* 几百行代码 */ }
class Stack extends Base { /* 几百行代码 */ }
// 复杂的 ref 管理
const startRef1 = useRef(null);
const scoreRef1 = useRef(null);
// ... 更多 ref重构后:
// 简洁的组件调用
<StackGame
ref={stackGame1Ref}
onScoreUpdate={handleScoreUpdate(1)}
onGameOver={handleGameOver(1)}
onGameStart={() => handleGameStart(1)}
/>阶段二:Socket 连接优化
问题诊断
原始代码中每次 setRoomCode 都会创建新的 Socket 连接:
const socket = io("http://localhost:9527"); // 每次渲染都执行解决方案
使用单例模式管理 Socket 连接:
let socket = null;
const initSocket = () => {
if (!socket) {
socket = io("http://localhost:9527");
// 事件监听器设置
}
return socket;
};遇到的问题与解决方案
问题一:游戏逻辑错误
问题描述
重构后的游戏组件无法正确实现堆叠效果,方块会直接替换而不是切割堆叠。
问题分析
对比原始代码发现重叠检测算法有误:
错误的算法:
const distance = Math.abs(currentPosition - prevPosition);
const overlap = edge - distance;正确的算法:
const direction = Math.sign(currentPosition - prevPosition);
const overlap = edge + direction * (prevPosition - currentPosition);解决过程
对比原始逻辑:仔细对比
index copy 2.js中的原始算法修复计算公式:使用正确的重叠距离计算公式
修复位置计算:正确计算重叠方块和切掉方块的位置
修复创建顺序:按照原始逻辑的顺序创建和移除方块
问题二:游戏状态异常
问题描述
第二次点击就会直接重启游戏,控制台显示:
Click detected, current level: 0 gameState: paused gameover: false
Starting game...问题分析
通过添加调试信息发现问题根源:
useEffect 重复执行:
useEffect(() => {
// 游戏实例创建
}, [onScoreUpdate, onGameOver, onGameStart, debug]); // 依赖数组有问题回调函数重新创建:每次父组件渲染时,回调函数都会重新创建,导致 useEffect 重新执行
游戏实例被重置:每次 useEffect 执行都会创建新的游戏实例,重置所有状态
解决方案
分离创建和更新逻辑:
// 只在组件挂载时创建一次
useEffect(() => {
// 创建游戏实例
}, []); // 空依赖数组
// 单独更新回调函数
useEffect(() => {
if (stackInstanceRef.current) {
stackInstanceRef.current.onScoreUpdate = onScoreUpdate;
stackInstanceRef.current.onGameOver = onGameOver;
stackInstanceRef.current.onGameStart = onGameStart;
}
}, [onScoreUpdate, onGameOver, onGameStart]);问题三:事件监听器管理
问题描述
点击事件可能被重复绑定,导致游戏行为异常。
解决方案
优化点击逻辑判断:
onClick() {
this.renderer.domElement.addEventListener("click", () => {
// 添加详细的状态检查
if (this.gameover) return;
if (this.level === 0 && !this.gamestart) {
this.start();
} else if (this.gameState === "running") {
this.detectOverlap();
}
});
}调试策略
1. 分步调试
在关键方法中添加
console.log跟踪游戏状态变化
对比原始代码的执行流程
2. 状态监控
console.log('detectOverlap debug:', {
currentPosition,
prevPosition,
direction,
edge,
overlap,
moveAxis,
moveEdge,
level: this.level
});3. 生命周期追踪
useEffect(() => {
console.log('useEffect running, creating new Stack instance');
// ...
return () => {
console.log('useEffect cleanup, destroying Stack instance');
};
}, []);重构成果
代码结构优化
重构前:
主文件:800+ 行代码
混杂的逻辑:游戏 + UI + Socket
难以维护和扩展
重构后:
游戏组件:600+ 行纯游戏逻辑
主页面:200+ 行 UI 和状态管理
清晰的职责分离
复用性提升
// 可以轻松创建多个游戏实例
<StackGame ref={player1Ref} debug={false} />
<StackGame ref={player2Ref} debug={true} />维护性改善
单一职责:每个组件只负责自己的逻辑
接口清晰:通过 props 和 ref 进行通信
状态隔离:游戏状态和 UI 状态分离
经验总结
重构最佳实践
逐步重构:不要一次性重构所有代码
保留原始代码:作为参考和对比
充分测试:每个重构步骤都要验证功能正确性
详细调试:添加足够的调试信息帮助定位问题
React 开发注意事项
useEffect 依赖管理:
谨慎使用依赖数组
区分创建和更新逻辑
避免不必要的重新执行
组件生命周期:
合理管理资源的创建和销毁
避免内存泄漏
状态管理:
明确状态的归属
避免状态冲突
Three.js 集成经验
资源管理:正确释放 Three.js 资源
事件处理:合理管理事件监听器
性能优化:避免不必要的渲染循环
项目结构
src/
├── components/
│ └── StackGame.js # 独立的游戏组件
├── routes/
│ └── Game/
│ ├── index.js # 主游戏页面(重构后)
│ ├── index copy 2.js # 原始代码备份
│ └── style.css # 样式文件
└── ...技术栈
React:组件化开发
Three.js:3D 图形渲染
GSAP:动画效果
Socket.io:实时通信
Ant Design:UI 组件库
总结
通过这次重构,我们不仅解决了原有代码的问题,还积累了宝贵的开发经验。重构过程中遇到的问题让我们更深入地理解了 React 组件生命周期、Three.js 资源管理和 JavaScript 事件处理机制。
最重要的是,我们学会了:
系统性思考:从整体架构角度思考问题
问题定位:通过调试信息快速定位问题根源
渐进式改进:逐步优化而不是推倒重来
文档化:记录问题和解决方案,便于后续维护
这次重构为项目的后续开发奠定了良好的基础,也为团队积累了宝贵的技术经验。
阶段六:双人游戏状态管理优化
问题发现
在测试双人游戏功能时,发现了几个关键的状态管理问题:
重试按钮状态错误:JSX 中使用了未定义的
showRetry变量,而不是正确的player1ShowRetry和player2ShowRetry重试点击事件错误:两个玩家的重试按钮都调用了通用的
handleRetry函数,而不是各自独立的重试函数游戏开始状态冲突:使用全局的
gameStarted状态导致一个玩家的操作影响另一个玩家的 UI 显示游戏结束状态干扰:每次单个玩家游戏结束就设置全局
gameState为 "ended",影响了双人游戏体验
修复方案
1. 修复重试按钮状态和事件绑定
修改前:
// 玩家1
<div className={classNames("status retry h-center fade-in cursor-pointer", { "opacity-0": !showRetry })} onClick={handleRetry}>
Try Again
</div>
// 玩家2
<div className={classNames("status retry h-center fade-in cursor-pointer", { "opacity-0": !showRetry })} onClick={handleRetry}>
Try Again
</div>
修改后:
// 玩家1
<div className={classNames("status retry h-center fade-in cursor-pointer", { "opacity-0": !player1ShowRetry })} onClick={handleRetryPlayer1}>
Try Again
</div>
// 玩家2
<div className={classNames("status retry h-center fade-in cursor-pointer", { "opacity-0": !player2ShowRetry })} onClick={handleRetryPlayer2}>
Try Again
</div>
2. 添加独立的游戏开始状态管理
添加状态:
// 为每个玩家分别管理游戏开始状态
const [player1Started, setPlayer1Started] = useState(false);
const [player2Started, setPlayer2Started] = useState(false);更新游戏开始回调:
const handleGameStart = (playerNumber) => {
console.log(`Player ${playerNumber} started game`);
if (playerNumber === 1) {
setPlayer1Started(true);
} else {
setPlayer2Started(true);
}
setGameStarted(true); // 保留全局状态用于其他逻辑
};更新UI显示:
// 玩家1
<div className={classNames("status start h-center fade-in", { "opacity-0": player1Started })}>
点击开始
</div>
// 玩家2
<div className={classNames("status start h-center fade-in", { "opacity-0": player2Started })}>
点击开始
</div>
3. 优化重试函数逻辑
修改前:
const handleRetryPlayer1 = () => {
setPlayer1Score(0);
setPlayer1ShowRetry(false);
setGameStarted(false); // 会影响玩家2
// ...
};修改后:
const handleRetryPlayer1 = () => {
setPlayer1Score(0);
setPlayer1ShowRetry(false);
setPlayer1Started(false); // 只重置玩家1的开始状态
// ...
};
const handleRetryPlayer2 = () => {
setPlayer2Score(0);
setPlayer2ShowRetry(false);
setPlayer2Started(false); // 只重置玩家2的开始状态
// ...
};4. 移除游戏结束时的全局状态设置
修改前:
const handleGameOver = (playerNumber) => (finalScore) => {
// ...
setGameState("ended"); // 影响双人游戏
};修改后:
const handleGameOver = (playerNumber) => (finalScore) => {
// ...
// 不需要设置全局游戏状态,因为这是双人游戏
// setGameState("ended");
};修复效果
通过这些修复,实现了:
独立的重试体验:每个玩家的"Try Again"按钮只在自己游戏结束时显示
隔离的游戏状态:一个玩家的重试不会影响另一个玩家的游戏状态
正确的UI交互:每个玩家的"点击开始"提示独立显示和隐藏
清晰的状态管理:每个玩家的游戏状态完全独立,互不干扰
技术要点
状态独立性:为双人游戏中的每个玩家维护独立的状态变量
事件隔离:确保玩家的操作只影响自己的游戏实例和状态
UI一致性:保持每个玩家区域的UI显示逻辑一致且独立
调试友好:通过清晰的变量命名和日志输出,便于问题定位和调试