react-three实现3D游戏(4)——小地图

发布于:2024-05-08 ⋅ 阅读:(24) ⋅ 点赞:(0)

概述

游戏中使用小地图来查看玩家当前的位置,几乎是必然出现的功能,这次我们在react-three中实现小地图的功能。

方案

这里提供2种方案:

  1. 正交相机
  2. 一张图片

如果你有更好的方案,欢迎在评论区告诉我,不胜感激!

使用正交相机

最偷懒和最快的方案,就是在场景中创建一个正交相机。把它放在游戏场景的正上方,位置刚好能看到全部地图。

再把这个正交相机的内容动态渲染到固定在右上角落的Html元素上。

如官网示例: image.png

这样小地图实际上就成为了卫星相机所观察到的场景,而且不用考虑玩家位置的更新。因为相机中能看到玩家的模型。

但是,它的性能不佳,因为多引入一个相机。相应的渲染对性能影响较大。

  1. 资源消耗增加: 每个相机都需要额外的计算资源来处理视角、投影等操作,因此在一个场景中引入多个相机会增加系统的资源消耗。
  2. 渲染时间增加: 每个相机都需要进行一次完整的渲染过程,包括对场景中的对象进行遍历、投影、光照等处理。因此,引入多个相机会增加整体渲染时间,导致性能下降。
  3. GPU 负载增加: 对于 WebGL 渲染,每个相机都会触发一次 GPU 渲染操作。因此,引入多个相机会增加 GPU 的负载,可能导致渲染效率降低,甚至出现性能瓶颈。

使用图片

这个就很简单了,我们用一张图片来作为小地图。将3d世界的坐标转换到这个图片上的坐标,再不断更新玩家的位置。

因为只有一张图片的加载及更新坐标,所以对性能的开销很小。但坐标转换稍微涉及到一点高中数学知识。需要稍微运用一下空间想象能力。

添加小地图

综合上述,我们使用第二种方案:即使用图片作为小地图,将玩家坐标转换到图片上的位置,并更新。

获取图片

虽然我们不使用第一种方案,但我们可以用正交相机来获取图片。

参考react-three的示例:

进入models文件夹下的index.tsx文件,将当前的相机换成正交相机,注释掉玩家挂载。

<Canvas style={{ width: '100%', height: '100%' }} orthographic
    camera={{
      up: [0, 1, 0],
      position:[0,50,0]
    }}
  >
    <Suspense fallback={null}>
      <Lights />
      <Sky distance={500} sunPosition={[200, 300, 100]} />
      <Ocean range={500} />

      <Physics>
        <Floor />
      </Physics>
      <axesHelper args={[155]} />
      <MapControls enableRotate={false} />
    </Suspense>
  </Canvas>

注意这里的坐标辅助线的长度是155,这是我调整后的长度,以它的x,z轴构成矩形,能刚好把地图囊括进去。

image.png 如图,红线和蓝线分别代表x轴和z轴,以它们的原点为中心构成的矩形恰好囊括地图。

我们把这部分裁剪出来,作为小地图的背景图。它对应3D场景的宽高都是310个坐标单位。

显示小地图

把前面处理好的图片放到public目录下的img文件夹中,命名为map.jpg

在pages文件夹下新建miniMap.tsx,我们把地图的背景图片显示出来。并使用一个蓝色圆点来表示玩家的位置,玩家默认位置在原点,所以初始化玩家元素居中。

export default function MiniMap() {
 return <div className={"miniMap"} >
      <div className="map">
        <img src="/img/map.jpg" />
        <div className="player" style={style}>
          <div className="point" />
        </div>
      </div>
    </div>
}

再添加一些简单样式,主要将小地图给个宽高固定在页面右上方。

image.png

添加个点击放大的功能,在less中添加一下transition设置。让他移动到屏幕中心并放大4倍。

image.png

更新玩家位置

现在我们有了地图了,需要做的就是更新地图中央的蓝点。仔细观察我们的地图,我们获得的玩家坐标属于以地图中央为原点的坐标系统,其中红线x,蓝线z轴。

而我们使用绝对定位来定位玩家元素,使用的是相对于地图元素左上角为原点的坐标系统。

image.png

坐标转换

我们可以很容易将中央为原点的坐标系统,转移到左上为原点的坐标系统。

试想下,在中心坐标中的原点(x:0,y:0),在左上坐标系中是多少呢? 因为我们之前截图时,特意计算了整个地图在坐标中的宽高是310单位。所以是 (x:155 ,z: 155)。

只要将玩家3d坐标中的x,z值分别在 x轴和z轴上 加上155个单位。就会变成左上坐标系的坐标。将这个坐标除以边的长度310。即得到距离左上原点的百分比坐标。

坐标转换代码如下:

  // 坐标转换为以左上角为原点的百分比坐标
  function convertToPercentage(x = 0, y = 0, boxWidth = 310, boxHeight = 310) {
    // 将原始坐标移动到盒子的左上角作为新的原点
    const newX = x + boxWidth / 2;
    const newY = y + boxHeight / 2;

    // 将坐标值转换为百分比
    const leftPercent = (newX / boxWidth) * 100;
    const topPercent = (newY / boxHeight) * 100;

    return { top: `${topPercent}%`, left: `${leftPercent}%` };
  }

更新位置

我们已经成功完成了坐标转换函数,接下来只要不断的获得玩家的坐标即可。

小地图所在的pages模块,与models模块互相之间在架构是并行解耦的。为了获得models中玩家的位置,我们使用zustand进行数据管理。运行npm i zustand添加依赖。

在src下新建stores文件夹,并新建文件models.tsx,定义声明玩家坐标的变量和更新函数。

import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import * as THREE from "three";

export const useModels =create(
subscribeWithSelector<ModelsState>((set) => {
  return {
    playerPos: undefined,
    update: (payload: Partial<ModelsState>) => {
      set((state) => {
        return { ...state, ...payload };
      });
    }
  };
})
);
type ModelsState = {
  playerPos: THREE.Vector3 | undefined;
  update: (payload: Partial<ModelsState>) => void;
};

进入player.tsx, 给Ecctrl元素添加名称player 使用getObjectByNamescene中直接获取玩家的mesh,再将其存储到stores中。由于数据的引用关系,后续只要在小地图中不断轮询玩家位置即可。

  ...
  // 存储玩家的位置
  const player = useThree((state) => state.scene).getObjectByName("player");
  const { update } = useModels();
  useEffect(() => {
    player && update({ playerPos: player.position });
  }, [player]);
  
  return (
    <Ecctrl
    name='player'
    ...
    />
   ...

最后回到小地图,我们来轮询获取玩家位置,并将位置转换为百分比坐标应用到样式中。

附全部代码:

  const [style, setStyle] = useState({ top: '50%', left: '50%' });
  // 不要解构赋值, 直接引用store的值
  const playerPos = useModels((state) => state.playerPos);
  
  useEffect(() => {
    updatePosition();
    return () => {
      clearTimeout(timer);
    };
  }, [playerPos]);
  
  // 定时更新位置
  function updatePosition() {
    if (!playerPos) return;
    const { x, z } = playerPos;
    setStyle(convertToPercentage(x, z))
    timer = setTimeout(() => {
      updatePosition();
    }, 1500);
  }
 // 坐标转换为以左上角为原点的百分比 
 function convertToPercentage(...){...}

 return (
    <div className={ "miniMap"}>
      <div className="map">
        <img src="/img/map1.jpg" />
        <div className="player" style={style}>
          <div className="point" />
        </div>
      </div>
    </div>
  );

结语

本次完成

  • 添加小地图

最终效果

我们站在桥边的z轴上,小地图上也显示同样的位置,这个小地图的精度已经非常不错了。

image.png

image.png

项目地址

🔗

结语

我等了春天秋天,等一句好久不见,可是一转眼已经是夏天,原来一切已时过境迁。

陌生人,希望你依然清澈,依然有云的从容,风的自由。

如果有什么能让这个3d游戏更有趣的想法,请在评论区告诉我.


网站公告

今日签到

点亮在社区的每一天
去签到