地图如今在游戏中发挥的作用越来越重要,随着电子竞技的兴起,地图逐渐成为了为玩家创造体验的直接舞台。希望本文能对有兴趣了解游戏地图背后实现原理的同学一些帮助。
什么是游戏地图
在游戏中可以通过3D场景虚拟一个完整的世界,当3D场景较为广阔和地形比较复杂时,游戏玩家在场景中漫游行走,往往容易因为陌生的环境而迷失方向。而通过为玩家提供一张场景地图,就可以以一种更加直观的形式向他们呈现整个游戏的世界观。游戏地图通常是为以用户俯视的视角构建出来的描述场景地形环境的地图,例如平原、山地、树林和主干道等,并会显示各处的关键地点。
游戏地图的作用
▐ 沉浸式体验
在很多游戏中,地图能够为玩家带来有更加沉浸式的体验。例如像赛车或者奔跑类的游戏,对地图的依赖性很高,游戏的乐趣也往往体现在地图上,通过地图能够唤起、激发玩家对游戏的兴趣,为玩家带来独特的游戏代入感。游戏代入感的一个重要来源就是能够让玩家感同深受,比如玩家控制的角色可以在虚拟的世界里奔跑飞翔,完成一些任务可能需要途径很多地方,在这过程中,设计地图实时更新玩家当前的位置,让游戏本身的设计就与地图的设计相辅相成。
▐ 指路功能
当玩家初次进入复杂且场景范围较大的3D场景中,往往容易迷失方向,对自己当前身处的位置以及场景的全貌没有一个准确的认知。就像现实生活中出远门需要地图导航一样,在3D场景中同样需要为玩家提供一张3D场景对应的地图来承担指路功能,让玩家能清楚地意识到自己前进的方向,以及场景中存在什么建筑物体,就能保证玩家不会在复杂的3D场景地形地貌中迷路。如果是射击类的游戏,在地图中还可以标注出敌人的位置或补给物体的位置,为玩家解锁更多有意思的玩法。
正俯视视角地图的技术实现
▐ 需求说明
- 前期准备
一张3D场景的正俯视角度下,与场景XZ平面1:1还原的地图图片素材。
图1 正俯视视角地图
- 实现目标
用户在3D场景内漫游移动时,能够在地图中实时更新用户的位置,并且只展示用户当前位置所对应的地图局部区域,如下图左上角区域所示。
图2 效果展示图
▐ 技术实现
- 技术方案简述
当我们拥有一张根据3D场景正俯视视角下的地图图片时,用户在3D场景中的实时位置,从俯视视角看下来时就是用户应该位于2D地图所对应的位置。如此,当用户在场景中跑动时,我们计算人物在场景XZ平面上的百分比,即可还原出在2D地图中的百分比。
但是需求里并非简单地将俯视图作为地图进行完全的展示,要求是只展示用户所在位置的局部区域,这样我们可以让地图作为展示区域的背景图,进行局部展示,在人物移动时,移动地图即可形成相对运动的效果。
- 实现步骤
- 绘制地图可见区域框,整张地图图片以canvas元素的形式加载,并作为可见区域背景图
- 人物在2D正俯视地图的位置,作为可见区域的中心
- 获取场景的AABB包围盒,得到3D场景的XZ平面宽高
- 计算人物当前位置在场景XZ平面的百分比
- 进而更新人物在地图图片上的百分比
- 人物行走,通过移动背景地图图片,更新人物实时位置
- 非边缘跑动:地图动,人物不动
- 边缘跑动:地图不动,人物动
图3 计算占比示意图
- 实现效果说明
有些同学可能会提出疑惑,为何采用canvas绘制地图,而非普通dom元素?
原因是由于IOS机型在使用CSS属性clipPath对可见区域边框裁剪,溢出部分仍可响应触摸事件。改用canvas重新实现,解决了这个问题,并且性能比用dom节点实现更好。
非正俯视视角地图的技术实现
▐ 需求说明
- 前期准备
3D场景任意俯视视角下的图片作为场景的大地图,并提供该视角下相机位置、旋转、缩放、远近平面、fov等参数。获得非正俯视视角地图的一个巧妙方法,在unity内查看场景模型时,调整一个合适的位置,截图可得。
图4 非正俯视视角地图
图5 俯视图相机参数
- 实现目标
需要实现在相机任意俯视视角下,都能准确还原3D场景中的人物当前位置。
▐ 技术实现
- 概述
现在终于来到了非正俯视视角地图中,实时更新当前位置的具体技术的实现部分了。
读到这里,有些同学可能会有些疑问了,非正俯视视角地图和正俯视视角地图里都有人物当前位置对应地图的位置,为什么非正俯视视角的地图实现的方式需要赘述一次呢?
其实大家简单想象一下就可以发现,假设人物当前位置静止,当我们以上帝视角(即相机)去俯视3D场景时,从不同角度或位置观察3D场景,得到的如图6、图7所示的俯视图图片是不一样的,人物位置相对地图图片的百分比自然也是不一样的。如此,在非正俯视视角下,我们就不可能采取类似于正俯视视角的按计算百分比的方案去还原人物当前位置。
图6 远俯视角度
图7 近俯视角度
- 解决思路
由于所有我们看到的渲染结果,都是视锥体投影到近平面的结果,如果我们把地图的图片放置到近平面上,再从当前相机位置向3D场景中的人物位置发出一条射线,那么该射线与近平面(也就是地图)相交的交点,也就是当前人物的在地图上的实时位置。那么这个方法具体实现起来,可以分成以下这几个步骤。
- 定义一个地图所在的平面:法向是相机的朝向,距离是近平面的距离。
其中相机的朝向在相机坐标系下可以设置为单位方向(0,0,-1),需要将该坐标变换到世界坐标系下,即应用V逆矩阵的变换得到相机朝向在世界坐标系下的表示。
定义一个平面,除了相机朝向(也就是平面的法向)外,还需要在这个法向上的距离,即近平面的距离0.3。如此,得到世界坐标系下地图所在的平面的定义。 - 定义一条射线:世界坐标系下,从相机位置,到人物当前位置,发出一条射线。
- 得到一个交点:世界坐标系下,射线与平面的交点,是一个世界坐标系下的3D交点坐标。
- 将世界坐标系下的交点坐标,重新转为在相机坐标系下的3D坐标,并去掉不需要的z轴信息。
世界坐标系转相机坐标系,即将该点做V变换,平移加XYZ轴对齐。 - 此时相机坐标系下的点在平面的比例,就是地图中的比例。
计算近平面的宽高值,宽高值可以通过视锥体的aspect、fov、近平面距离等轻松求出。
根据unity在相机坐标系是左手系,得到地图的左上角坐标值。
计算当前交点相对于地图图片的百分比即可。
- 步骤分解
- 定义一个地图所在的平面:法向是相机的朝向,距离是近平面的距离0.3const localN = new Vector(0,0,-1)const worldN = localN.apply(ModelMatrix)const plane = new Plane(worldN.normalize(), near)
- 定义一条射线:从相机位置,到人物当前位置,发出一条射线const ray = new Ray(ICameraPos, dir.normalize());
- 得到平面与射线的一个交点:射线与平面的交点,是一个世界坐标系下的3D坐标 targetPointray.intersectPlane(plane, targetPoint);
- 将世界坐标下的3D坐标转为相机坐标系下的3D坐标 : 相机平移+3个轴对齐--相机坐标系下看向Z轴targetPoint = targetPoint.subtract(ICameraPos);targetPoint = targetPoint.apply(ModelMatrix.inverse());
- 求近平面的宽高const halfH = Mat(8)tan(fov*0.5) * near;const aspect = imgW / imgH;const halfW = halfH * aspect;const leftTop = new Vector2(-halfW,halfH);
- 此时相机坐标系下的点在平面的比例,就是地图中的比例const px = Mat(8)abs(target2DP.x - leftTop.x) / (2 * halfW);const py = Mat(8)abs(target2DP.y - leftTop.y) / (2 * halfH);
实现效果
通过视频效果的展示,可以看出,当用户在3D场景中向右走,然后到达右侧交叉路口再向右走时,重新回到地图页,当前位置图标已经更新到对应的正确位置。
结语
地图如今在游戏中发挥的作用越来越重要,随着电子竞技的兴起,地图逐渐成为了为玩家创造体验的直接舞台。希望本文能对有兴趣了解游戏地图背后实现原理的同学一些帮助。
参考资料:浅谈游戏中的小地图与大地图设计(地址:https://zhuanlan.zhihu.com/p/504211689)
团队介绍
我们是天猫技术品牌线的行业前端团队,目前负责消费电子、3C数码、运动、家装家居、汽车、奢品等行业的线上线下模式的探索,面向淘内淘外,提供商家、门店、消费者最佳用户体验。团队在XR、3D、2D渲染引擎这些创新体验上有不错的沉淀,同时面向全栈领域团队探索了 Serverless 云端研发模式,在消费者前台,通过数据挖掘消费、意图识别提升消费者效率,同样面向工程领域,在跨端、前端工程化、中后台微前端都有一些沉淀,如果你是一位充满想象的终端极客,欢迎你的加入,通过自己的技术想法去改变天猫行业的终端表达。