# 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();
});