「我為歌狂」遊戲製作
发表于 : 2026年 6月 13日 22:38
做一個html+canvas+js遊戲,3d可旋轉。畫布大小為600*600。起初底板為480*480淺藍色正方形,底板沒有高度,邊緣為虛線白色。
遊戲等於分割成縱横都是八塊區域,每一塊為60*60,一共64個棋位。設從左到右坐標為0,1,2,3,4,5,6,7,從上到下坐標為a b,c,d,e,f,g,h。後面將用坐標表示棋位,如(2,d)。
中間初始放置有四個遊戲板,每個遊戲板的大小為120*120,其表面為木黄色,其邊緣為淺灰色,中心線為十字黑色,線寬為2,高度為4。每個板佔四個棋位,這四個遊戲板分別佔的坐標是[2,c;3,c;2,d;3,d],[4,c;5,c;4,d;5,d],[2,e;3,e;2,f;3,f],[4,e;5,e;4,f;5,f]。
當鼠標或觸控旋轉視圖時,如果上下移動,將是俯視角移至側視角,但不能翻到底面;如果左右移動,將等於順時針或逆時針旋轉視圖,但不能超過45度。
隨機放置兩個圓柱體至遊戲板上的其中兩個棋位,圓柱體高48,直徑48,表面為紫色,線寬為1,線色為黑色。
隨機放置兩個立方體,至遊戲板上的另兩個空棋位,長高寬都是48,表面為橙色和深藍色,六個面分別為骰子六個點,其中一個立方體的所有點的形狀是正三角形,另一個立方體的所有點的形狀是正六邊形,點的顏色分別為淺綠色和淺黄色。
立方體起初為一點朝上。當點擊其中一個立方體,它將會升起48,然後繞其3d中心旋轉至二點,然後再落下。如果已經是二點,將旋轉至三點……如果是六點,將旋轉至一點。
gemini 3.1 pro
遊戲等於分割成縱横都是八塊區域,每一塊為60*60,一共64個棋位。設從左到右坐標為0,1,2,3,4,5,6,7,從上到下坐標為a b,c,d,e,f,g,h。後面將用坐標表示棋位,如(2,d)。
中間初始放置有四個遊戲板,每個遊戲板的大小為120*120,其表面為木黄色,其邊緣為淺灰色,中心線為十字黑色,線寬為2,高度為4。每個板佔四個棋位,這四個遊戲板分別佔的坐標是[2,c;3,c;2,d;3,d],[4,c;5,c;4,d;5,d],[2,e;3,e;2,f;3,f],[4,e;5,e;4,f;5,f]。
當鼠標或觸控旋轉視圖時,如果上下移動,將是俯視角移至側視角,但不能翻到底面;如果左右移動,將等於順時針或逆時針旋轉視圖,但不能超過45度。
隨機放置兩個圓柱體至遊戲板上的其中兩個棋位,圓柱體高48,直徑48,表面為紫色,線寬為1,線色為黑色。
隨機放置兩個立方體,至遊戲板上的另兩個空棋位,長高寬都是48,表面為橙色和深藍色,六個面分別為骰子六個點,其中一個立方體的所有點的形狀是正三角形,另一個立方體的所有點的形狀是正六邊形,點的顏色分別為淺綠色和淺黄色。
立方體起初為一點朝上。當點擊其中一個立方體,它將會升起48,然後繞其3d中心旋轉至二點,然後再落下。如果已經是二點,將旋轉至三點……如果是六點,將旋轉至一點。
gemini 3.1 pro
代码: 全选
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>3D 棋盤互動遊戲</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #2c3e50;
}
#game-container {
width: 600px;
height: 600px;
background-color: #000;
position: relative;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
canvas {
display: block;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="game-container"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 1. 初始化場景、相機、渲染器
const container = document.getElementById('game-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x34495e);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 2000);
// 設置初始視角
camera.position.set(0, 450, 450);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(600, 600);
// 支援 sRGB 顏色空間,讓材質顏色更鮮豔
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
// 2. 設置光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(200, 400, 200);
scene.add(dirLight);
// 3. 視角控制器 (OrbitControls)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false; // 禁止平移
controls.enableZoom = true;
// 限制上下移動(俯視至側視,不可翻到底面)
controls.minPolarAngle = 0; // 0 度為正上方俯視
controls.maxPolarAngle = Math.PI / 2; // 90 度為平行側視
// 限制左右移動(不能超過 45 度)
controls.minAzimuthAngle = -Math.PI / 4;
controls.maxAzimuthAngle = Math.PI / 4;
// 4. 建立底板 (480x480)
const boardSize = 480;
const planeGeo = new THREE.PlaneGeometry(boardSize, boardSize);
const planeMat = new THREE.MeshLambertMaterial({ color: 0xADD8E6 }); // 淺藍色
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotation.x = -Math.PI / 2;
scene.add(plane);
// 底板白色虛線邊緣
const edgesGeo = new THREE.EdgesGeometry(planeGeo);
const edgesMat = new THREE.LineDashedMaterial({ color: 0xffffff, dashSize: 10, gapSize: 10 });
const planeEdges = new THREE.LineSegments(edgesGeo, edgesMat);
planeEdges.computeLineDistances(); // 必須呼叫才能顯示虛線
planeEdges.rotation.x = -Math.PI / 2;
planeEdges.position.y = 0.5; // 微幅抬高防止 Z-fighting
scene.add(planeEdges);
// 5. 建立四個遊戲板 (120x120, 高度 4)
// 繪製遊戲板表面材質 (木黃色 + 黑色十字)
function createBoardTopTexture() {
const canvas = document.createElement('canvas');
canvas.width = 120; canvas.height = 120;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#DEB887'; // 木黃色
ctx.fillRect(0, 0, 120, 120);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(60, 0); ctx.lineTo(60, 120);
ctx.moveTo(0, 60); ctx.lineTo(120, 60);
ctx.stroke();
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
const topBoardMat = new THREE.MeshLambertMaterial({ map: createBoardTopTexture() });
const sideBoardMat = new THREE.MeshLambertMaterial({ color: 0xD3D3D3 }); // 淺灰色
// Three.js Box 材質順序: 右, 左, 上, 下, 前, 後
const boardMaterials = [sideBoardMat, sideBoardMat, topBoardMat, sideBoardMat, sideBoardMat, sideBoardMat];
const gameBoardGeo = new THREE.BoxGeometry(120, 4, 120);
const boardCenters = [
{ x: -60, z: -60 }, // 佔 [2,c;3,c;2,d;3,d]
{ x: 60, z: -60 }, // 佔 [4,c;5,c;4,d;5,d]
{ x: -60, z: 60 }, // 佔 [2,e;3,e;2,f;3,f]
{ x: 60, z: 60 } // 佔 [4,e;5,e;4,f;5,f]
];
boardCenters.forEach(pos => {
const board = new THREE.Mesh(gameBoardGeo, boardMaterials);
board.position.set(pos.x, 2, pos.z); // y=2 是因為高度為4,中心在2
scene.add(board);
});
// 6. 決定隨機棋位 (16個遊戲板上的棋位)
// a=0, b=1, c=2, d=3, e=4, f=5, g=6, h=7
const validCells = [
[2,2], [3,2], [2,3], [3,3], // Board 1
[4,2], [5,2], [4,3], [5,3], // Board 2
[2,4], [3,4], [2,5], [3,5], // Board 3
[4,4], [5,4], [4,5], [5,5] // Board 4
];
// 打亂陣列以隨機取樣
for(let i = validCells.length - 1; i > 0; i--){
const j = Math.floor(Math.random() * (i + 1));
[validCells[i], validCells[j]] = [validCells[j], validCells[i]];
}
// 座標轉換函式 (行列轉成世界 X Z)
function getCellWorldPos(col, row) {
return {
x: (col - 3.5) * 60,
z: (row - 3.5) * 60
};
}
const itemY = 4 + 24; // 板高4 + 物體半高24 = 28
// 7. 放置隨機兩個圓柱體
for(let i = 0; i < 2; i++) {
const cell = validCells[i];
const pos = getCellWorldPos(cell[0], cell[1]);
const cylGeo = new THREE.CylinderGeometry(24, 24, 48, 32);
const cylMat = new THREE.MeshLambertMaterial({ color: 0x800080 }); // 紫色
const cylinder = new THREE.Mesh(cylGeo, cylMat);
cylinder.position.set(pos.x, itemY, pos.z);
const cylEdgesGeo = new THREE.EdgesGeometry(cylGeo);
const cylEdgesMat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 1 });
const cylEdges = new THREE.LineSegments(cylEdgesGeo, cylEdgesMat);
cylinder.add(cylEdges);
scene.add(cylinder);
}
// 8. 放置隨機兩個立方體 (骰子)
const cubes = [];
// 生成骰子每一面的材質
function createDiceMaterial(baseColor, dotColor, shape, number) {
const canvas = document.createElement('canvas');
canvas.width = 128; canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, 128, 128);
ctx.fillStyle = dotColor;
const drawShape = (x, y) => {
ctx.beginPath();
if (shape === 'triangle') {
const r = 16;
ctx.moveTo(x, y - r);
ctx.lineTo(x + r*0.866, y + r*0.5);
ctx.lineTo(x - r*0.866, y + r*0.5);
} else { // hexagon
const r = 14;
for (let i=0; i<6; i++) {
const angle = i * Math.PI / 3;
if(i===0) ctx.moveTo(x + r*Math.cos(angle), y + r*Math.sin(angle));
else ctx.lineTo(x + r*Math.cos(angle), y + r*Math.sin(angle));
}
}
ctx.closePath();
ctx.fill();
};
const dots = [];
if(number === 1) dots.push([64, 64]);
if(number === 2) dots.push([32, 32], [96, 96]);
if(number === 3) dots.push([32, 32], [64, 64], [96, 96]);
if(number === 4) dots.push([32, 32], [96, 32], [32, 96], [96, 96]);
if(number === 5) dots.push([32, 32], [96, 32], [64, 64], [32, 96], [96, 96]);
if(number === 6) dots.push([32, 24], [32, 64], [32, 104], [96, 24], [96, 64], [96, 104]);
dots.forEach(p => drawShape(p[0], p[1]));
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return new THREE.MeshLambertMaterial({ map: texture });
}
// 將數字 1~6 對應至 BoxGeometry 的面: [右(+x), 左(-x), 上(+y), 下(-y), 前(+z), 後(-z)]
const faceMapping = [3, 4, 1, 6, 2, 5];
function createDiceMesh(baseColor, dotColor, shape) {
const materials = faceMapping.map(num => createDiceMaterial(baseColor, dotColor, shape, num));
const cube = new THREE.Mesh(new THREE.BoxGeometry(48, 48, 48), materials);
// 狀態管理
cube.userData = {
currentNumber: 1,
animating: false,
startTime: 0,
startQuat: new THREE.Quaternion(),
targetQuat: new THREE.Quaternion()
};
return cube;
}
// 立方體 1: 橙色底, 淺綠色正三角形點
const cube1Pos = getCellWorldPos(validCells[2][0], validCells[2][1]);
const cube1 = createDiceMesh('#FFA500', '#90EE90', 'triangle');
cube1.position.set(cube1Pos.x, itemY, cube1Pos.z);
scene.add(cube1);
cubes.push(cube1);
// 立方體 2: 深藍色底, 淺黃色正六邊形點
const cube2Pos = getCellWorldPos(validCells[3][0], validCells[3][1]);
const cube2 = createDiceMesh('#00008B', '#FFFFE0', 'hexagon');
cube2.position.set(cube2Pos.x, itemY, cube2Pos.z);
scene.add(cube2);
cubes.push(cube2);
// 9. 互動與動畫處理
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
container.addEventListener('pointerdown', (event) => {
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(cubes);
if (intersects.length > 0) {
const clickedCube = intersects[0].object;
if (!clickedCube.userData.animating) {
triggerCubeAnimation(clickedCube);
}
}
});
function triggerCubeAnimation(cube) {
cube.userData.animating = true;
cube.userData.startTime = performance.now();
cube.userData.startQuat.copy(cube.quaternion);
// 決定下一個數字
let nextNum = cube.userData.currentNumber + 1;
if (nextNum > 6) nextNum = 1;
cube.userData.currentNumber = nextNum;
// 計算對應的目標旋轉歐拉角
let rx = 0, ry = 0, rz = 0;
switch(nextNum) {
case 1: break; // +y 向上
case 2: rx = -Math.PI/2; break; // +z 向上
case 3: rz = Math.PI/2; break; // +x 向上
case 4: rz = -Math.PI/2; break; // -x 向上
case 5: rx = Math.PI/2; break; // -z 向上
case 6: rx = Math.PI; break; // -y 向上
}
cube.userData.targetQuat.setFromEuler(new THREE.Euler(rx, ry, rz));
}
// 10. 主渲染迴圈
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
// 處理所有立方體的動畫
cubes.forEach(cube => {
if (cube.userData.animating) {
const elapsed = now - cube.userData.startTime;
const duration = 1000; // 動畫總時長 1000 毫秒
let t = elapsed / duration;
if (t >= 1) {
t = 1;
cube.userData.animating = false;
}
// 拋物線高度:利用 sin 曲線呈現平滑的升起48再落下
cube.position.y = itemY + Math.sin(t * Math.PI) * 48;
// 3D 旋轉插值 (Slerp) 實現自然翻轉
cube.quaternion.slerpQuaternions(cube.userData.startQuat, cube.userData.targetQuat, t);
}
});
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>