在 Three.js 中实现 2D 小地图跟随玩家移动

0 阅读2分钟

引言

在 Three.js 的三维场景中,很多时候我们需要为玩家提供一个 2D 小地图,以便他们能够快速了解自己的位置和周围环境。本教程将演示如何在 Three.js 中实现一个 2D 小地图,它会随着玩家的移动而平滑调整,确保玩家始终保持在小地图的可见范围内。

需求分析

  1. 小地图是一张大尺寸 2D 图片,代表整个场景。
  2. 小地图窗口 只显示大地图的一部分,相当于视口。
  3. 玩家在小地图中移动,实时反映 3D 场景中的位置。
  4. 当玩家接近边界时,小地图平滑移动,确保玩家始终在视口范围内。

代码实现

1. HTML 结构

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js 2D 小地图</title>
    <style>
        body { margin: 0; overflow: hidden; position: relative; }
        canvas { display: block; }

        /* 小地图窗口 */
        #miniMap {
            position: absolute;
            bottom: 10px;
            right: 10px;
            width: 200px;
            height: 200px;
            border: 2px solid white;
            border-radius: 10px;
            overflow: hidden;
        }

        /* 小地图背景 */
        #mapContainer {
            position: relative;
            width: 800px;
            height: 800px;
            background: url('mini-map.png') no-repeat;
            background-size: cover;
        }

        /* 玩家标记 */
        #playerDot {
            position: absolute;
            width: 10px;
            height: 10px;
            background: red;
            border-radius: 50%;
            transform: translate(-50%, -50%);
        }
    </style>
</head>
<body>
    <script type="module">
        import * as THREE from 'https://cdn.jsdelivr.net/npm/three@latest/build/three.module.js';

        // 创建 Three.js 场景
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 10, 20);
        camera.lookAt(0, 0, 0);

        // 渲染器
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 创建地面
        const groundGeometry = new THREE.PlaneGeometry(20, 20);
        const groundMaterial = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, side: THREE.DoubleSide });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2;
        scene.add(ground);

        // 玩家(立方体)
        const playerGeometry = new THREE.BoxGeometry(1, 1, 1);
        const playerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        const player = new THREE.Mesh(playerGeometry, playerMaterial);
        scene.add(player);

        // 小地图窗口
        const miniMap = document.createElement('div');
        miniMap.id = 'miniMap';
        document.body.appendChild(miniMap);

        // 小地图背景
        const mapContainer = document.createElement('div');
        mapContainer.id = 'mapContainer';
        miniMap.appendChild(mapContainer);

        // 玩家标记
        const playerDot = document.createElement('div');
        playerDot.id = 'playerDot';
        mapContainer.appendChild(playerDot);

        // 小地图参数
        const worldSize = 20;
        const mapSize = 800;
        const viewportSize = 200;
        const halfViewport = viewportSize / 2;

        function animate() {
            requestAnimationFrame(animate);
            player.position.x = Math.sin(Date.now() * 0.001) * 9;
            player.position.z = Math.cos(Date.now() * 0.001) * 9;
            updateMiniMap(player.position);
            renderer.render(scene, camera);
        }

        function updateMiniMap(playerPosition) {
            const minX = -worldSize / 2, maxX = worldSize / 2;
            const minZ = -worldSize / 2, maxZ = worldSize / 2;

            const mapX = ((playerPosition.x - minX) / (maxX - minX)) * mapSize;
            const mapY = ((playerPosition.z - minZ) / (maxZ - minZ)) * mapSize;

            playerDot.style.left = `${mapX}px`;
            playerDot.style.top = `${mapY}px`;

            let offsetX = mapX - halfViewport;
            let offsetY = mapY - halfViewport;

            offsetX = Math.max(0, Math.min(offsetX, mapSize - viewportSize));
            offsetY = Math.max(0, Math.min(offsetY, mapSize - viewportSize));

            mapContainer.style.transform = `translate(${-offsetX}px, ${-offsetY}px)`;
        }

        animate();
    </script>
</body>
</html>

代码解析

  1. Three.js 场景:创建了一个带有地面的 3D 场景,并添加了一个代表玩家的立方体。

  2. 小地图:使用 div 元素创建了一个 #miniMap 窗口,内含 #mapContainer 作为整个大地图。

  3. 玩家标记#playerDot 代表玩家在小地图中的位置。

  4. 更新小地图

    • 计算玩家在小地图的坐标
    • 确保玩家始终可见:如果玩家接近小地图边界,则平滑移动 #mapContainer

总结

通过本教程,实现了一个 Three.js 3D 场景与 2D 小地图联动的效果。小地图能够动态跟随玩家移动,并在接近边缘时平滑调整,使玩家始终可见。你可以进一步扩展该功能,例如:

  • 小地图旋转 适配相机角度。
  • 缩放小地图,适应不同的场景需求。
  • 显示多个标记点(NPC、任务目标等)。

🚀