分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5888
注册时间: 2022年 11月 18日 17:36
联系:

「我為歌狂」遊戲製作

帖子 ejsoon »

做一個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

代码: 全选

<!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>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: 没有注册用户 和 0 访客