高德地图游戏解决方案——结合threejs仿某猫捉猫猫得红包游戏

体验地址

体验地址浏览器F12打开控制台以手机调试模式效果更佳,模型稍大 耐心等待

源码下载地址

源码下载地址

技术栈

  • vite
  • threejs
  • nipplejs

前言

现如今数字化时代,商圈为吸引顾客和增加顾客黏性,能够让人顾客有更好的购物体验和参与感,移动端小游戏应运而生,而今天要介绍的项目就是通过虚实结合制作的小游戏,通过高德提供的地图,结合threejs写一个趣味小游戏,项目主要是通过用户操作手柄控制角色移动,领取任务和做任务,文中只介绍一条任务线,通过手柄控制角色去抓鸡,当然,你也可以做一个寻宝游戏,或者其他类型的游戏。

访问体验地址时,浏览器调为手机模式,效果更佳

游戏流程:领取任务--> 抓鸡 --> 完成任务

准备AMAP地图

src/content/AMapScene.ts路径下createAMap方法创建基础地图服务,并返回一个promise,返回一个地图服务AMap,地图实例map,自定义图层customLayer和自定义坐标系customCoords。需要注意的是为了能体验最新版本的APIAMAPLoca的版本统一2.0,Loca为数据可视化API。

接下来就是根据map返回的坐标系customCoords和其他参数创建一个可视化图层,具体参数可以参考ThreeJS 加载模型和路径动画,在createGLCustomLayer方法中创建AMap.GLCustomLayer时通过init创建渲染threejs所需的场景、镜头、渲染器等元素,相机采用PerspectiveCamera透视相机,需要配合创建map时候的参数 viewMode: '3D', 开启3D视图,默认为关闭,如果不需要用3d视角,可以结合# 正交相机(OrthographicCamera),具体API参考官网文档,渲染器采用WebGLRenderer,如果想用更强的渲染性能或者光追可以使用WebGPURender,不过作者没在高德地图上使用过,目前高德地图对threejs的只支持貌似只有r157左右,版本太高会报错。灯光和模型组也都可以在init中创建,render用来循环渲染,当然你也可以将其他需要渲染的内容也加进去,比如tweenjs的渲染和动画的渲染,本项目中添加了定时器的渲染和模型骨骼动画渲染,以callback的方式,在render的方法中进行调用

typescript 复制代码
var createGLCustomLayer = (map: any, AMap: any, customCoords: any, render: ()=>void): Promise<CreateGLCustomLayerType> => {
    return new Promise((resolve) => {
     const customLayer = new AMap.GLCustomLayer({
            zIndex: 110, // 图层的层级
            init: async (gl: any) => {},
            render: () => {
                map.render(); // 渲染地图
                render() // 调用传入的渲染函数
            },
        });
      resolve({
        customLayer
        })
    })
}

调用定时器的渲染和动画的渲染,后续内容会讲到。

typescript 复制代码
// 渲染函数
const render = () => {
    // 渲染小鸡骨骼动画
    chickenAnimation && chickenAnimation.upDate()
    // 渲染主角骨骼动画
    motorAnimation && motorAnimation.upDate()
    // 更新定时器
    intervalTime && intervalTime.update();
}

加载模型

主角模型采用fbx格式,小鸡模型和商店模型采用gltf格式,尽管不同的格式,但是对于目前项目的使用除了性能问题并没什么区别。实战中尽量用gltf

文件路径src/utils/loaders.ts封装了各种格式模型的加载器,直接通过加载器可以将模型加载到页面上,拿fbx举例:

文件路径src/content/handleModal.ts将所有模型一次性加载出来,如果考虑性能问题,可以将模型懒加载到页面,或者直接缓存到indexedDB中。

typescript 复制代码
 const player = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/Idle.fbx`);
typescript 复制代码
var position = customCoords.lngLatsToCoords(mapCenter.toArray())[0];
// 加载模型
const { player } = await handleModal();
// 旋转模型
player.rotation.set(Math.PI * 0.5, Math.PI, 0);
// 改变模型位置,将模型添加到地图正中央
player.position.copy(numArr2v3(position));
// 将主角添加到场景
scene.add(player);

主角是自带谷歌动画的,目前没渲染骨骼动画,所以默认T形,模型默认是躺在地上的,这并不是模型有问题,而是高德地图和threejs的坐标系之间的转换问题, scene.add(new THREE.AxesHelper(70)) // 添加坐标轴辅助可以加载一个辅助线观察一下,在threejs中蓝色代表z轴,而y轴是朝上的,在高德地图中,xy两个轴是平行于地图的,所以修改高度需要调整z轴,在后续的主角移动和旋转都需要注意这个问题。

map中的自定义坐标提供了一个方法可以将经纬度转化成3d世界所有的坐标,根据3d世界的中心点0,0,0和地图世界的中心点mapcenter计算出来的。返回3d世界的xy轴是个数字数组,而numArr2v3方法是将xy轴数组转为vector3,其中z轴作为可选参数默认是0。

后续会用相同的方式将糖果屋模型加载到地图上,文章就不赘述了。

动画

文件地址src/utils/animation.ts封装了一个加载骨骼动画的方法HandleAnimation,包含采集动画、播放动画、切换动画、播放和切换一次性动画(例如跳、嘲讽、手势)、更新等方法。并且upDate更新动画支持传入相机,可以使相机跟随主角的移动而跟随主角,在高德地图中并不需要用到。

而高德地图中的镜头跟随,有另一套规则,首先在创建自定义图层时的render中,相机的各种参数包括远近端位置,lookat和矩阵都是从自定义地图方法customCoords.getCameraParams中赋值的,所以不需要自己去修改相机的参数,只需要单纯的控制地图即可,在后面的人物控制中会讲到。

typescript 复制代码
 const player = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/Idle.fbx`);
    player.animations[0].name = 'idle'

    const walkP = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/walk.fbx`);
    walkP.animations[0].name = 'walk'

    const walk_2P = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/walk_2.fbx`);
    walk_2P.animations[0].name = 'walk_2'

    const talkP = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/talk.fbx`);
    talkP.animations[0].name = 'talk'
    const pick_up = await loadFbx(`${import.meta.env.VITE_ASSETS_URL}assets/model/fbx/pick_up.fbx`);

这段代码加载了很多主角的动画,包含站立、行走、对话、拾取等,其实如果有技术的话这些动画完全可以放在同一个fbx文件里,接下来修改一下加载主角部分代码,执行主角动画。

加载动画

typescript 复制代码
...已有的加载模型代码
playerAnimation = new HandleAnimation(player, animations);
playerAnimation.once(['pick_up'])
playerAnimation.play('idle');
...修改主角位置代码
scene.add(playerAnimation.model)

使用HandleAnimation构造函数创建主角的动画功能,animations为前文加载的动画fbx模型中包含的动作,调用once将pick_up拾取动画设置为只执行一次,构造函数中如果切换成once的动作,将在执行完成以后切换为上一个动画,主角默认执行idle闲置动画,将主角动画添加到场景。

这样我们就将带动画的主角添加到场景中了。

除了闲置动画,前文还加载了两种walk的方法,动画名称为walk是正常速度走路,在没领取任务的时候和距离抓捕目标距离太远的情况下使用,动画名称为walk_2为静悄悄走路,为了真实还原抓鸡的过程,在主角靠近到抓捕目标的一定距离内,都采用这种方式走路。动画名称为pick_up为拾取(这个动画有点问题),当主角与抓捕目标模型有交集的时候(碰撞检测),则播放该动画

控制人物方向

控制人物行走首先需要一个手柄,项目中采用nipplejs工具类,在屏幕中间生成一个可操作手柄,过多的不介绍,简单说一下提供的几个回调,在文件src/utils/nipple.ts中封装了创建手柄的方法,接受三个callback,开始移动,移动中,移动结束,三个方法对应主角的不同动画和方法。

开始移动

typescript 复制代码
// 开始移动玩家
const startPlayer = () => {
    playerAnimation.fadeToAction('walk');
    run()
}

调用fadeToAction方法切换为行走动画,并开始移动动画,run方法中主要修改主角位置、方向、高德地图视角、下面在介绍。

移动中

typescript 复制代码
// 移动玩家
const movePlayer = (data: any) => {
    deg = data.angle.degree
}

移动中的回调主要是将当前手柄的角度赋值给全局变量deg,以便在run方法中修改主角方向时使用。

移动结束

javascript 复制代码
// 停止玩家移动
const endPlayer = () => {
    playerAnimation.fadeToAction('idle');
    intervalTime.removeIntervalById(intervalId)
}

结束移动,将主角动画切换为空闲,并将移动的定时器删除。这个定时器也是在run方法中定义的

run方法内容解析

主角方向

typescript 复制代码
 const euler = deg2Euler(deg);
// 设置玩家模型的旋转角度
playerAnimation.model.rotation.copy(euler);

前文中movePlayer的回调方法中储存的deg角度在这里就用到了,将角度转成欧拉角,并赋值给模型的rotation属性,角度转欧拉角需要先用threejs提供的MathUtils.degToRad工具将角度转为弧度, new Euler(Math.PI * 0.5, radians + Math.PI * 0.5, 0, 'XYZ');,不仅要沿着y轴方向旋转,还要在主角原有的旋转角度基础上进行计算,这样主角就能保证站立状态旋转。

主角移动

typescript 复制代码
// 获取玩家当前位置的副本
const copyPos = playerAnimation.model.position.clone()
// 根据欧拉角和移动速度计算新的位置
const offsetPos = euler2Matrix(copyPos, euler, mute ? 0.2 : 0.8)

// 更新玩家模型的位置并将Z轴设置为0
playerAnimation.model.position.copy(offsetPos).setZ(0);

要想计算出主角的下一个位置,需要当前位置、旋转欧拉角、加速度这几个必要条件,主要用到四元数、三维向量、乘积这些数学相关的知识,这里我就不班门弄斧了,大家看代码吧

typescript 复制代码
// 基于欧拉角改变向量位置
export const euler2Matrix = (originalVector: Vector3, eulerAngles: Euler, distance = 1) => {
    // 1. 将Euler转换为Quaternion  
    const quaternion = new Quaternion().setFromEuler(eulerAngles);

    // 2. 创建一个单位方向向量(例如沿Z轴)  
    const directionVector = new Vector3(0, 0, 1);

    // 3. 使用Quaternion旋转方向向量  
    directionVector.applyQuaternion(quaternion);

    // 4. 将旋转后的方向向量乘以距离,并将其加到原始向量上  
    const movedVector = originalVector.clone().addScaledVector(directionVector, distance);

    return movedVector;
}

镜头跟踪

文件src/utils/index.ts中封装了getViewCp3d坐标转屏幕坐标、coordsToLngLats屏幕坐标转高德经纬度,根据前文计算出的主角位置,再通过这两个方法就可以设置地图视角,而设置好的地图视角,又会被自定义图层中的render方法赋值给camera,这样往复循环,便实现了高德地图镜头跟踪方法。

typescript 复制代码
// 屏幕坐标
const screenPos = new Vector2()
// 获取玩家模型在屏幕上的位置
getViewCp(playerAnimation.model.position.clone(), screenPos, camera);

// 地图坐标
const mapPos = new Vector2();
// 将屏幕坐标转换为地图坐标
coordsToLngLats(screenPos, AMap, map, mapPos);
// 设置地图中心为玩家位置
map.setCenter(mapPos.toArray())

玩家坐标转屏幕坐标---屏幕坐标转高德经纬度---根据经纬度设置mapcenter

模型间交互

在角色抓捕目标或者和npc交互的时候,都需要计算模型之间的交互,下面介绍一下距离判断和模型之间的碰撞检测

判断距离

主角行走过程中,判断主角和抓捕目标的距离,当距离达到一定范围时,切换行走方式

makeTask方法中获取主角和抓捕目标之间的距离

typescript 复制代码
// 获取小鸡和玩家的位置
const A = chickenAnimation.model.position.clone()
const B = playerAnimation.model.position.clone()
// 计算小鸡和玩家之间的距离
const distance = A.distanceTo(B);

当distance小于某个距离的时候切换walk_2动画,否则执行walk动画,并设置mute变量,当mute为true时,获取主角下一个位置的时候加速度为0.2,行走速度变慢; 否则为0.8,行走速度变快。

typescript 复制代码
euler2Matrix(copyPos, euler, mute ? 0.2 : 0.8)

碰撞检测

由于没有添加物理引擎,所以这里的碰撞检测使用包围盒box3,当两个box3有交集的时候视为碰撞

typescript 复制代码
// 创建一个包围盒并设置为小鸡模型的包围盒
const box = new Box3()
box.setFromObject(chickenAnimation.model);

// 创建一个包围盒并设置为玩家模型的包围盒
const box2 = new Box3()
box2.setFromObject(playerAnimation.model);

box2.intersectsBox(box)

检测使用box3包围盒提供的intersectsBoxAPI,后续如果要做角色交付任务时与npc的检测也可以使用

小鸡的操作

基于前面的内容,添加一个小鸡模型,收集动作,播放动画,设置位置,这些不再详细介绍,主要介绍一下小鸡的行走路线,小鸡有自己的一套行动路线,并在行走结束的时候播放吃食、闲置等动作

路径规划

typescript 复制代码
 // 规划一条路线并让这个模型沿着这条路线移动,并在移动的过程中正确的展示模型的方向,使模型始终朝着行动的方向
const path = new CatmullRomCurve3([
    numArr2v3(customCoords.lngLatsToCoords([120.188767, 30.193832])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189102, 30.194031])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189198, 30.193524])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189371, 30.192999])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189565, 30.192896])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189654, 30.192632])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189746, 30.192867])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189524, 30.192999])[0]),
    numArr2v3(customCoords.lngLatsToCoords([120.189353, 30.193057])[0]),
]);

// 获取路径上的所有点
const points = path.getPoints(800);

创建一条平滑的三维样条曲线,并获取到路径上所有的点位信息,下面这段我用AI写的,大概是循环每一个点位,根据不同点位移动小鸡,结束的时候使用setInterval,切换不同的动画。

typescript 复制代码
// 遍历路径上的所有点
points.forEach((p, index) => {
    i++
    // 设置一个定时器,按照顺序移动小鸡到路径上的每个点
    const timeOut = setTimeout(() => {
        // 更新小鸡模型的位置
        chickenAnimation.model.position.copy(p)
        // 获取下一个点
        const lookat = points[index + 1];
        if (lookat) {
            // 计算当前点和下一个点之间的角度
            const angle = calculateAngle(p, lookat)
            // 设置小鸡模型的旋转角度
            chickenAnimation.model.rotation.set(Math.PI * 0.5, Math.PI * 0.5 + angle, 0)
        }
        if (index >= points.length - 1) {

            // 如果到达路径的最后一个点,清除定时器,并切换到Peck动画
            clearTimeout(timeOut);
            chickenAnimation.fadeToAction('Peck');
            chickedMove = false
            // 设置一个间隔定时器,循环切换小鸡的动画
            setInterval(() => {
                j++
                if (j % 2 === 0) {
                    chickenAnimation.fadeToAction('Twerk')
                } else if (j % 3 === 0) {
                    chickenAnimation.fadeToAction('Peck')
                } else {
                    chickenAnimation.fadeToAction('Idle')
                }
            }, 1000)
        }
    }, i * 40)
})

结语

这只是一个游戏的操作层面的基础,可以根据高德地图获取周边商家,为每一个商家发布任务,供玩家领取,不同任务对应不同价值优惠券之类的促销活动,需要注意的是对于模型的选择,需要短小精悍的,而不是像本项目中20M的模型,一个动作都500kb,对于手机压力太大了,好在是在高德地图基础上绘制的3d世界,高德地图至少能对性能做些许优化

历史作品

three.js 专栏

threejs------从实战出发之智慧塔吊大屏

three.js------完整3d大屏展示超详细讲解

相关推荐
Thomas游戏开发7 分钟前
Unity3D 逻辑服的Entity, ComponentData与System划分详解
前端框架·unity3d·游戏开发
zhangjr05751 小时前
【HarmonyOS Next】鸿蒙实用装饰器一览(一)
前端·harmonyos·arkts
不爱学习的YY酱1 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
木子七1 小时前
vue2-vuex
前端·vue
麻辣_水煮鱼2 小时前
vue数据变化但页面不变
前端·javascript·vue.js
BY—-组态2 小时前
web组态软件
前端·物联网·工业互联网·web组态·组态
一条晒干的咸魚2 小时前
【Web前端】实现基于 Promise 的 API:alarm API
开发语言·前端·javascript·api·promise
WilliamLuo2 小时前
MP4结构初识-第一篇
前端·javascript·音视频开发
Beekeeper&&P...2 小时前
web钩子什么意思
前端·网络
啵咿傲3 小时前
重绘&重排、CSS树&DOM树&渲染树、动画加速 ✅
前端·css