# 3D魔方游戏
这是一个基于Three.js的3D魔方游戏,支持2到6阶魔方的模拟操作。
## 功能特点
- 支持2到6阶魔方
- 真实的3D渲染效果
- 鼠标操作控制
- 随机打乱功能
- 提示功能
- 重置功能
### 安装依赖
```bash
npm install
```
### 启动游戏
```bash
npm start
```
然后在浏览器中访问 `http://localhost:8080` 即可开始游戏。
## 操作说明
- 使用鼠标拖拽可以旋转整个魔方
- 按住Shift键并点击魔方的某一面可以旋转该面
- 使用界面上的下拉菜单可以选择魔方的阶数(2到6阶)
- 点击"随机打乱"按钮可以随机打乱魔方
- 点击"提示"按钮可以获取下一步的提示
- 点击"重置"按钮可以重置魔方到初始状态
## 技术栈
- HTML5
- CSS3
- JavaScript (ES6+)
- Three.js (3D渲染库)
## 浏览器兼容性
支持所有现代浏览器,包括:
- Chrome
- Firefox
- Safari
- Edge
## 许可证
ISC
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import TWEEN from 'three/examples/jsm/libs/tween.module.js';
import { Cube } from './cube.js';// 全局变量
let scene, camera, renderer, controls;
let cube;
let currentOrder = 3; // 默认3阶魔方
let isCtrlPressed = false; // 跟踪Ctrl键是否按下
let isDragging = false; // 跟踪是否正在拖拽// 初始化场景
function init() {// 创建场景scene = new THREE.Scene();scene.background = new THREE.Color(0xf0f0f0);// 获取容器const container = document.getElementById('cube-container');const containerWidth = container.clientWidth;const containerHeight = container.clientHeight || window.innerHeight * 0.6;// 强制设置容器高度container.style.height = `${containerHeight}px`;// 创建相机camera = new THREE.PerspectiveCamera(50, // 视角更广containerWidth / containerHeight,0.1,1000);// 根据魔方阶数调整相机位置const cameraDistance = 8 + currentOrder * 1.0; // 增加距离camera.position.set(cameraDistance, cameraDistance, cameraDistance);camera.lookAt(0, 0, 0);// 创建渲染器renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setSize(containerWidth, containerHeight);renderer.setPixelRatio(window.devicePixelRatio);container.appendChild(renderer.domElement);// 添加轨道控制controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.1;controls.minDistance = cameraDistance * 0.5;controls.maxDistance = cameraDistance * 2;controls.enableRotate = true;controls.rotateSpeed = 1.0;// 允许完全旋转controls.minPolarAngle = 0;controls.maxPolarAngle = Math.PI;controls.minAzimuthAngle = -Infinity;controls.maxAzimuthAngle = Infinity;controls.target.set(0, 0, 0);// 默认禁用轨道控制controls.enabled = false;// 添加光源const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);scene.add(ambientLight);const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(10, 20, 15);scene.add(directionalLight);// 添加第二个方向光源,照亮底部const bottomLight = new THREE.DirectionalLight(0xffffff, 0.6);bottomLight.position.set(-5, -10, -7);scene.add(bottomLight);// 添加第三个方向光源,照亮侧面const sideLight = new THREE.DirectionalLight(0xffffff, 0.6);sideLight.position.set(-10, 5, -10);scene.add(sideLight);// 创建魔方createCube(currentOrder);// 添加窗口大小调整监听window.addEventListener('resize', onWindowResize);// 添加交互控制setupInteraction();
}// 窗口大小调整
function onWindowResize() {const container = document.getElementById('cube-container');const containerWidth = container.clientWidth;const containerHeight = container.clientHeight || window.innerHeight * 0.6;camera.aspect = containerWidth / containerHeight;camera.updateProjectionMatrix();renderer.setSize(containerWidth, containerHeight);
}// 动画循环
function animate() {requestAnimationFrame(animate);TWEEN.update(); // 更新动画controls.update();renderer.render(scene, camera);
}// 创建魔方
function createCube(order) {if (cube) {scene.remove(cube.group);}cube = new Cube(order);scene.add(cube.group);
}// 设置交互控制
function setupInteraction() {const raycaster = new THREE.Raycaster();const mouse = new THREE.Vector2();let selectedFace = null;let startPoint = new THREE.Vector2();let endPoint = new THREE.Vector2();// 监听Ctrl键window.addEventListener('keydown', function(event) {if (event.key === 'Control') {isCtrlPressed = true;controls.enabled = true;renderer.domElement.style.cursor = 'move';console.log('Ctrl键按下,启用轨道控制');}});window.addEventListener('keyup', function(event) {if (event.key === 'Control') {isCtrlPressed = false;controls.enabled = false;renderer.domElement.style.cursor = 'default';console.log('Ctrl键释放,禁用轨道控制');}});// 添加初始提示const infoDiv = document.createElement('div');infoDiv.style.position = 'absolute';infoDiv.style.bottom = '10px';infoDiv.style.left = '10px';infoDiv.style.backgroundColor = 'rgba(0,0,0,0.7)';infoDiv.style.color = 'white';infoDiv.style.padding = '5px 10px';infoDiv.style.borderRadius = '5px';infoDiv.style.fontSize = '14px';infoDiv.innerHTML = '按住Ctrl键可自由旋转整个魔方<br>点击或拖动魔方面可旋转该面';document.getElementById('cube-container').appendChild(infoDiv);// 10秒后隐藏提示setTimeout(() => {infoDiv.style.opacity = '0';infoDiv.style.transition = 'opacity 1s';setTimeout(() => {infoDiv.remove();}, 1000);}, 10000);// 初始自动旋转魔方,让用户看到所有面setTimeout(() => {// 先旋转到一个角度,让用户看到更多面const startRotation = { x: 0, y: 0 };const endRotation = { x: Math.PI / 3, y: Math.PI / 4 };new TWEEN.Tween(startRotation).to(endRotation, 1500).easing(TWEEN.Easing.Quadratic.Out).onUpdate(() => {cube.group.rotation.x = startRotation.x;cube.group.rotation.y = startRotation.y;}).onComplete(() => {// 旋转完成后,重置魔方位置setTimeout(() => {new TWEEN.Tween(cube.group.rotation).to({ x: 0, y: 0, z: 0 }, 1000).easing(TWEEN.Easing.Quadratic.Out).start();}, 1000);}).start();}, 500);// 鼠标按下事件renderer.domElement.addEventListener('mousedown', function(event) {// 如果按下Ctrl键,启用轨道控制并跳过魔方面旋转if (event.ctrlKey || isCtrlPressed) {controls.enabled = true;return;}// 禁用轨道控制controls.enabled = false;// 如果魔方正在动画中,则不处理if (cube && cube.isAnimating) return;isDragging = false;const rect = renderer.domElement.getBoundingClientRect();mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;// 保存起始点startPoint.set(event.clientX, event.clientY);endPoint.copy(startPoint); // 初始化终点与起点相同raycaster.setFromCamera(mouse, camera);try {const allCubies = cube.getAllCubies();if (!allCubies || allCubies.length === 0) {console.warn('没有找到魔方小块');return;}// 递归设置为true,以检测子对象const intersects = raycaster.intersectObjects(allCubies, true);if (intersects.length > 0) {// 确保我们有正确的对象和面let targetObject = intersects[0].object;// 如果点击的是边缘线段,获取其父对象(实际的方块)while (targetObject.parent && !(targetObject instanceof THREE.Mesh)) {targetObject = targetObject.parent;}// 创建一个新的交点对象,确保有正确的目标对象和面信息const correctedIntersect = {...intersects[0],object: targetObject};// 尝试获取面信息selectedFace = cube.getFaceFromIntersect(correctedIntersect);if (selectedFace) {console.log('选中面:', selectedFace);renderer.domElement.style.cursor = 'pointer';} else {console.log('未能确定选中的面');// 尝试直接从物体位置确定面const position = targetObject.position.clone();const x = Math.round((position.x + cube.offset) / (cube.cubeSize + cube.gap));const y = Math.round((position.y + cube.offset) / (cube.cubeSize + cube.gap));const z = Math.round((position.z + cube.offset) / (cube.cubeSize + cube.gap));// 确定是哪个面if (x === 0) {selectedFace = { axis: 'x', value: -1, layer: 0 };} else if (x === cube.order - 1) {selectedFace = { axis: 'x', value: 1, layer: cube.order - 1 };} else if (y === 0) {selectedFace = { axis: 'y', value: -1, layer: 0 };} else if (y === cube.order - 1) {selectedFace = { axis: 'y', value: 1, layer: cube.order - 1 };} else if (z === 0) {selectedFace = { axis: 'z', value: -1, layer: 0 };} else if (z === cube.order - 1) {selectedFace = { axis: 'z', value: 1, layer: cube.order - 1 };}if (selectedFace) {console.log('从位置推断的面:', selectedFace);renderer.domElement.style.cursor = 'pointer';}}} else {console.log('未选中任何面');selectedFace = null;}} catch (error) {console.error('射线检测错误:', error);}});// 鼠标移动事件window.addEventListener('mousemove', function(event) {// 如果按下Ctrl键,让轨道控制处理移动if (event.ctrlKey || isCtrlPressed) {controls.enabled = true;return;}// 如果没有选中面,则不处理if (!selectedFace) return;// 更新终点位置endPoint.set(event.clientX, event.clientY);// 计算移动距离const moveDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));// 如果移动距离超过阈值,标记为拖拽if (moveDistance > 8) { // 增加阈值,减少误触isDragging = true;renderer.domElement.style.cursor = 'grabbing';// 计算拖拽方向向量const dragVector = {x: endPoint.x - startPoint.x,y: endPoint.y - startPoint.y};// 显示拖拽方向指示const direction = determineRotationDirection(selectedFace, dragVector);console.log('当前拖拽方向:', direction > 0 ? '顺时针' : '逆时针');}});// 鼠标释放事件window.addEventListener('mouseup', function(event) {renderer.domElement.style.cursor = 'default';// 如果按下Ctrl键,让轨道控制处理释放if (event.ctrlKey || isCtrlPressed) {return;}// 如果没有选中面,则不处理if (!selectedFace) return;// 更新终点位置endPoint.set(event.clientX, event.clientY);// 如果是拖拽操作,根据拖拽方向确定旋转方向if (isDragging) {// 计算拖拽方向向量const dragVector = {x: endPoint.x - startPoint.x,y: endPoint.y - startPoint.y};// 计算移动距离const moveDistance = Math.sqrt(Math.pow(dragVector.x, 2) + Math.pow(dragVector.y, 2));// 只有当移动距离足够大时才执行旋转,防止误触if (moveDistance > 15) {// 根据拖拽方向和选中的面确定旋转方向const direction = determineRotationDirection(selectedFace, dragVector);console.log('旋转方向:', direction);// 执行旋转if (cube && typeof cube.rotateFace === 'function') {cube.rotateFace({axis: selectedFace.axis,layer: selectedFace.layer,direction: direction});} else {console.error('cube.rotateFace 不是一个函数');}} else {console.log('移动距离不足,取消旋转');}} // 如果是点击操作,使用默认方向旋转else {console.log('点击操作');if (cube && typeof cube.rotateFace === 'function') {cube.rotateFace(selectedFace);} else {console.error('cube.rotateFace 不是一个函数');}}selectedFace = null;isDragging = false;});// 添加触摸支持renderer.domElement.addEventListener('touchstart', function(event) {if (cube && cube.isAnimating) return;event.preventDefault();isDragging = false;const rect = renderer.domElement.getBoundingClientRect();const touch = event.touches[0];mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;// 保存起始点startPoint.set(touch.clientX, touch.clientY);endPoint.copy(startPoint);raycaster.setFromCamera(mouse, camera);try {const allCubies = cube.getAllCubies();if (!allCubies || allCubies.length === 0) return;const intersects = raycaster.intersectObjects(allCubies, true);if (intersects.length > 0) {// 确保我们有正确的对象和面let targetObject = intersects[0].object;// 如果点击的是边缘线段,获取其父对象(实际的方块)while (targetObject.parent && !(targetObject instanceof THREE.Mesh)) {targetObject = targetObject.parent;}// 创建一个新的交点对象const correctedIntersect = {...intersects[0],object: targetObject};selectedFace = cube.getFaceFromIntersect(correctedIntersect);console.log('触摸选中面:', selectedFace);}} catch (error) {console.error('触摸检测错误:', error);}});renderer.domElement.addEventListener('touchmove', function(event) {if (!selectedFace) return;event.preventDefault();const touch = event.touches[0];// 更新终点位置endPoint.set(touch.clientX, touch.clientY);// 计算移动距离const moveDistance = Math.sqrt(Math.pow(endPoint.x - startPoint.x, 2) + Math.pow(endPoint.y - startPoint.y, 2));// 如果移动距离超过阈值,标记为拖拽if (moveDistance > 10) {isDragging = true;}});renderer.domElement.addEventListener('touchend', function(event) {if (!selectedFace) return;event.preventDefault();if (isDragging) {// 计算拖拽方向向量const dragVector = {x: endPoint.x - startPoint.x,y: endPoint.y - startPoint.y};// 根据拖拽方向和选中的面确定旋转方向const direction = determineRotationDirection(selectedFace, dragVector);// 执行旋转if (cube && typeof cube.rotateFace === 'function') {cube.rotateFace({axis: selectedFace.axis,layer: selectedFace.layer,direction: direction});}} else {if (cube && typeof cube.rotateFace === 'function') {cube.rotateFace(selectedFace);}}selectedFace = null;isDragging = false;});// 阻止右键菜单renderer.domElement.addEventListener('contextmenu', function(event) {event.preventDefault();});
}// 根据拖拽方向和选中的面确定旋转方向
function determineRotationDirection(face, dragVector) {const { axis, value } = face;// 计算拖拽的主要方向和角度const dragAngle = Math.atan2(dragVector.y, dragVector.x) * 180 / Math.PI;console.log('拖拽角度:', dragAngle);// 根据角度确定拖拽的主要方向let dragDirection;if (dragAngle > -45 && dragAngle <= 45) {dragDirection = 'right';} else if (dragAngle > 45 && dragAngle <= 135) {dragDirection = 'down';} else if (dragAngle > 135 || dragAngle <= -135) {dragDirection = 'left';} else {dragDirection = 'up';}console.log('拖拽方向:', dragDirection);// 根据面的轴和拖拽方向确定旋转方向// 1表示顺时针,-1表示逆时针let direction = 1;switch (axis) {case 'x': // 左右面if (value > 0) { // 右面direction = (dragDirection === 'up' || dragDirection === 'right') ? 1 : -1;} else { // 左面direction = (dragDirection === 'up' || dragDirection === 'left') ? 1 : -1;}break;case 'y': // 上下面if (value > 0) { // 上面direction = (dragDirection === 'right' || dragDirection === 'down') ? 1 : -1;} else { // 下面direction = (dragDirection === 'right' || dragDirection === 'up') ? 1 : -1;}break;case 'z': // 前后面if (value > 0) { // 前面direction = (dragDirection === 'right' || dragDirection === 'up') ? 1 : -1;} else { // 后面direction = (dragDirection === 'left' || dragDirection === 'up') ? 1 : -1;}break;}console.log('旋转方向:', direction > 0 ? '顺时针' : '逆时针');return direction;
}// 初始化事件监听
function initEventListeners() {// 魔方阶数选择document.getElementById('cube-order').addEventListener('change', (event) => {currentOrder = parseInt(event.target.value);createCube(currentOrder);updateLayerButtons(); // 更新层按钮});// 随机打乱按钮document.getElementById('scramble-btn').addEventListener('click', () => {cube.scramble();});// 提示按钮document.getElementById('hint-btn').addEventListener('click', () => {cube.showHint();});// 重置按钮document.getElementById('reset-btn').addEventListener('click', () => {createCube(currentOrder);updateLayerButtons(); // 更新层按钮});// 旋转控制按钮document.getElementById('rotate-left').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'y', angle: Math.PI / 4 });});document.getElementById('rotate-right').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'y', angle: -Math.PI / 4 });});document.getElementById('rotate-up').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'x', angle: Math.PI / 4 });});document.getElementById('rotate-down').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'x', angle: -Math.PI / 4 });});document.getElementById('rotate-clockwise').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'z', angle: -Math.PI / 4 });});document.getElementById('rotate-counter-clockwise').addEventListener('click', () => {rotateCubeWithAnimation({ axis: 'z', angle: Math.PI / 4 });});// 层选择控制// 初始化层按钮updateLayerButtons();// 轴选择变化时更新层按钮document.getElementById('axis-select').addEventListener('change', updateLayerButtons);// 层旋转方向按钮document.getElementById('rotate-clockwise-layer').addEventListener('click', () => {rotateSelectedLayer(1); // 顺时针});document.getElementById('rotate-counter-clockwise-layer').addEventListener('click', () => {rotateSelectedLayer(-1); // 逆时针});// 添加键盘控制window.addEventListener('keydown', function(event) {// 如果魔方正在动画中,则不处理if (cube && cube.isAnimating) return;// 如果按下Ctrl键,启用轨道控制if (event.key === 'Control') {isCtrlPressed = true;controls.enabled = true;renderer.domElement.style.cursor = 'move';console.log('Ctrl键按下,启用轨道控制');return;}// 键盘控制魔方旋转switch (event.key) {// 旋转整个魔方case 'ArrowLeft':if (event.shiftKey) {rotateCubeWithAnimation({ axis: 'y', angle: Math.PI / 4 });}break;case 'ArrowRight':if (event.shiftKey) {rotateCubeWithAnimation({ axis: 'y', angle: -Math.PI / 4 });}break;case 'ArrowUp':if (event.shiftKey) {rotateCubeWithAnimation({ axis: 'x', angle: Math.PI / 4 });}break;case 'ArrowDown':if (event.shiftKey) {rotateCubeWithAnimation({ axis: 'x', angle: -Math.PI / 4 });}break;// 旋转魔方的面 (按键1-9对应九宫格位置)case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':const keyNum = parseInt(event.key);let layer = 0;let axis = 'z';let direction = 1;// 根据按键确定旋转的面和方向if (keyNum <= 3) { // 上层layer = cube.order - 1;axis = 'y';direction = keyNum === 1 || keyNum === 3 ? -1 : 1;} else if (keyNum <= 6) { // 中层layer = Math.floor(cube.order / 2);axis = 'y';direction = keyNum === 4 || keyNum === 6 ? -1 : 1;} else { // 下层layer = 0;axis = 'y';direction = keyNum === 7 || keyNum === 9 ? -1 : 1;}// 执行旋转if (event.altKey) { // Alt键按下时旋转X轴axis = 'x';} else if (event.ctrlKey) { // Ctrl键按下时旋转Z轴axis = 'z';}cube.rotateFace({axis: axis,layer: layer,direction: direction});break;}});window.addEventListener('keyup', function(event) {if (event.key === 'Control') {isCtrlPressed = false;controls.enabled = false;renderer.domElement.style.cursor = 'default';console.log('Ctrl键释放,禁用轨道控制');}});// 添加操作说明const keyboardInfo = document.createElement('div');keyboardInfo.className = 'keyboard-info';keyboardInfo.innerHTML = `<h3>键盘控制说明:</h3><p>- Shift + 方向键: 旋转整个魔方</p><p>- 数字键1-9: 旋转对应位置的面</p><p>- Alt + 数字键: 沿X轴旋转</p><p>- Ctrl + 数字键: 沿Z轴旋转</p><p>- 默认沿Y轴旋转</p>`;document.querySelector('.instructions').appendChild(keyboardInfo);
}// 更新层按钮
function updateLayerButtons() {if (!cube) return;const layerButtonsContainer = document.getElementById('layer-buttons');layerButtonsContainer.innerHTML = ''; // 清空现有按钮const axis = document.getElementById('axis-select').value;// 为每一层创建按钮for (let i = 0; i < cube.order; i++) {const button = document.createElement('button');button.className = 'layer-button';button.textContent = i + 1;button.dataset.layer = i;button.dataset.axis = axis;// 点击选择层button.addEventListener('click', function() {// 移除其他按钮的选中状态document.querySelectorAll('.layer-button').forEach(btn => {btn.classList.remove('selected');});// 添加当前按钮的选中状态this.classList.add('selected');});layerButtonsContainer.appendChild(button);}// 默认选中第一个按钮if (layerButtonsContainer.firstChild) {layerButtonsContainer.firstChild.classList.add('selected');}
}// 旋转选中的层
function rotateSelectedLayer(direction) {const selectedButton = document.querySelector('.layer-button.selected');if (!selectedButton || !cube) return;const layer = parseInt(selectedButton.dataset.layer);const axis = selectedButton.dataset.axis;cube.rotateFace({axis: axis,layer: layer,direction: direction});
}// 旋转整个魔方的动画函数
function rotateCubeWithAnimation({ axis, angle }) {if (!cube || !cube.group) return;const startRotation = { value: 0 };const endRotation = { value: angle };new TWEEN.Tween(startRotation).to(endRotation, 300).easing(TWEEN.Easing.Quadratic.Out).onUpdate(() => {if (axis === 'x') {cube.group.rotation.x += (endRotation.value - startRotation.value) / 10;} else if (axis === 'y') {cube.group.rotation.y += (endRotation.value - startRotation.value) / 10;} else if (axis === 'z') {cube.group.rotation.z += (endRotation.value - startRotation.value) / 10;}}).start();
}// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {init();initEventListeners();animate();
});