react-three实现3D游戏(4)——小地图
概述
游戏中使用小地图来查看玩家当前的位置,几乎是必然出现的功能,这次我们在react-three中实现小地图的功能。
方案
这里提供2种方案:
- 正交相机
- 一张图片
如果你有更好的方案,欢迎在评论区告诉我,不胜感激!
使用正交相机
最偷懒和最快的方案,就是在场景中创建一个正交相机。把它放在游戏场景的正上方,位置刚好能看到全部地图。
再把这个正交相机的内容动态渲染到固定在右上角落的Html元素上。
如官网示例: 🔗link
这样小地图实际上就成为了卫星相机所观察到的场景,而且不用考虑玩家位置的更新。因为相机中能看到玩家的模型。
但是,它的性能不佳,因为多引入一个相机。相应的渲染对性能影响较大。
- 资源消耗增加: 每个相机都需要额外的计算资源来处理视角、投影等操作,因此在一个场景中引入多个相机会增加系统的资源消耗。
- 渲染时间增加: 每个相机都需要进行一次完整的渲染过程,包括对场景中的对象进行遍历、投影、光照等处理。因此,引入多个相机会增加整体渲染时间,导致性能下降。
- GPU 负载增加: 对于 WebGL 渲染,每个相机都会触发一次 GPU 渲染操作。因此,引入多个相机会增加 GPU 的负载,可能导致渲染效率降低,甚至出现性能瓶颈。
使用图片
这个就很简单了,我们用一张图片来作为小地图。将3d世界的坐标转换到这个图片上的坐标,再不断更新玩家的位置。
因为只有一张图片的加载及更新坐标,所以对性能的开销很小。但坐标转换稍微涉及到一点高中数学知识。需要稍微运用一下空间想象能力。
添加小地图
综合上述,我们使用第二种方案:即使用图片作为小地图,将玩家坐标转换到图片上的位置,并更新。
获取图片
虽然我们不使用第一种方案,但我们可以用正交相机来获取图片。
参考react-three的示例:svg地图交互
进入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轴构成矩形,能刚好把地图囊括进去。
如图,红线和蓝线分别代表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>
}
再添加一些简单样式,主要将小地图给个宽高固定在页面右上方。
添加个点击放大的功能,在less中添加一下transition设置。让他移动到屏幕中心并放大4倍。
更新玩家位置
现在我们有了地图了,需要做的就是更新地图中央的蓝点。仔细观察我们的地图,我们获得的玩家坐标属于以地图中央为原点的坐标系统,其中红线x,蓝线z轴。
而我们使用绝对定位来定位玩家元素,使用的是相对于地图元素左上角为原点的坐标系统。
坐标转换
我们可以很容易将中央为原点的坐标系统,转移到左上为原点的坐标系统。
试想下,在中心坐标中的原点(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
使用getObjectByName
从scene
中直接获取玩家的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轴上,小地图上也显示同样的位置,这个小地图的精度已经非常不错了。
项目地址
结语
我等了春天秋天,等一句好久不见,可是一转眼已经是夏天,原来一切已时过境迁。
陌生人,希望你依然清澈,依然有云的从容,风的自由。
如果有什么能让这个3d游戏更有趣的想法,请在评论区告诉我.
转载自:https://juejin.cn/post/7365334891472355365