我又写出了被 Three.js 官推转发的项目?!(源码分享)
(??金石瓜分计划强势上线,速戳上图了解详情??)
0.???前置条件?? ?? 好久不见,亲爱的小伙伴们!欢迎来到这个充满童趣的技术分享!在儿童节即将到来之际,我特别准备了这篇轻松有趣的Three.js 实战指南。
?? 你将学到什么?Three.js基础应用3D定制化资源获取技巧简易Three.js游戏开发思路?? 无需担心难度!本文专为儿童节设计,技术内容简单易懂,只要掌握Three.js基础用法就能轻松上手!
1.??Page 预览?? 又一年儿童节将至,看着日历上跳动的数字,忽然意识到自己早已不是那个会收到节日礼物的小朋友了。现在的我,日复一日地在公司里:
敲着后台管理系统的CURD调试着微信小程序的接口对着需求文档发呆「?? 系统警报:检测到开发者快乐指数跌破警戒线!」
「?? 正在注入童趣补丁...」
「?? 强制启动儿童节特别模式....」
画面预览小心飞驰的车辆,收集道具,创造你的最高纪录!
新手指南PC 端:使用方向键或 WASD 控制小鸡移动手机端:滑动屏幕即可轻松操控(适配完美!)游玩的新手指南您可以在点击右上角的暂停按钮看到,并且您可以再此为自己的角色取名,以便可以在排行榜上找到自己相关地址??? PC 端正式版(建议使用魔法上网)[1]:cross-road-eight.vercel.app/[2]??? PC 调试模式(开发者专用,请勿作弊哦)[3]:cross-road-eight.vercel.app/#debug[4]????? GitHub 源码(欢迎 Star?)[5]:github.com/hexianWeb/C…[6]另外当前小游戏已经国际知名小游戏平台收录[7]:synthgamer.com/game/34[8],此网站不需要魔法。
此外如果您正在使用手机浏览这篇文章,我非常建议您直接点击进入游玩,因为这个游戏我已经做了移动端适配。滑动将会以最符合操纵直觉的操控方式呈现在您面前
(小提示: 游戏会记录你的最佳成绩,在死亡时将成绩发送至排行榜单!快来挑战好友吧!????)
本着开源精神与诚实创作的原则,我必须说明:本项目的核心机制灵感来源于业界前辈的智慧结晶。特别感谢:「Hunor Borbely」的优秀教程:《Crossy Road 风格游戏开发指南》[9]
参考内容有:
游戏元数据 (metadata) 的结构设计角色与车辆的碰撞检测逻辑(指路原作者)
?? 官方认可很荣幸这个项目同样获得了 Three.js 官推的转发认可:
(Tips: 站在巨人的肩膀上,我们才能看得更远!??)
2.?? Three.js 游戏三要素我之前在2025 年了,我不允许有前端不会用 Trae 让页面 Hero Section 变得高级!!!(Threejs)[10]提到过Three.js三要素,他们分别是场景、相机以及渲染器。
什么是游戏开发三要素?而在 Three.js 游戏开发中,我们同样有三大核心架构:
要素作用类比现实「Scene」游戏世界的 3D 环境就像游乐场的场地「Game UI」用户界面和交互层相当于游乐场的指示牌和售票处「Metadata」游戏数据和逻辑类似游乐场的运营规则和游客数据为什么这很重要?理解这三个要素的关系至关重要:
「Scene」负责 "演什么" - 处理 3D 模型、光照、物理效果「UI」负责 "怎么看" - 控制分数显示、菜单系统「Metadata」负责 "怎么玩" - 管理游戏状态、得分规则、角色属性3.??资源获取3D Model 资源生成在?????? 我写出了被 Threejs 官推转发的项目????![11]文章中我曾写过「3D 客制化资源的获取与处理」,这里我不做过多赘述,只做简单举例
就比小鸡模型的获取我先通过awesome-gpt4o-images[12]给的提示参考图以及相应的提示语生成一张背景单一,结构合理的2.5D小鸡图片,
我这里将参考图奉上,您有空也可以试试这些神奇的提示词
随后再将图片导入AI 3D generation平台,我比较喜欢的是hyper3D[13]。 过 30 秒即可生成对应的模型。看起来还不错。
游戏背景音乐资源获取游戏背景音乐则是使用Suno AI进行生成的。老实说我觉得还挺好听的。
但是如果想要获取真正的游戏背景音乐以及各种游戏音效可以访问Opengameart 的音乐板块[15]
?输入风格描述:"8-bit retro game music with cheerful melody"
?如果对于这块教程还有疑惑可以点击这里,在前一篇文章中我有着详细的介绍。
AI 工具让独立开发者也能拥有专业级美术资源, 随着 AI 数字资产的获取门槛降低,我相信未来对于我们网站开发人员来说真正的上限应该是「视觉想象力」与「数字美感素养」。
4.??metadata & 基础场景搭建现在让我们来看一看原版的小鸡过马路游戏场景
现在的首要目的是分析出当前场景所需的metadata, 在不考虑场景自生成的情况下利用metadata在Threejs中搭建出游戏的基本场景。
首先我们可以看到图中小鸡不断前进,场景会由数个不同的 “行” 拼接而成,他们可能是 “草地“,也可能是” 柏油路“然后每个行上都有着相应的物体,"草地" 上会出现高矮不一的 "树木",而 "柏油路" 上会出现行驶方向向左或者向右的汽车那么metadata应该如何更好的囊括这些信息呢?我是这样做的:
现在我们先采用静态metadata来构建初始游戏场景,通过模块化的设计实现草地、道路、树木和车辆的动态生成。
静态 metadata 结构constmetadata = [// 第一行 { type:'forest', trees: [ { tileIndex:-7,type:'tree01'}, { tileIndex:-3,type:'tree02'}, ], },// 第二行 { type:'road', direction:true, speed:1, vehicles: [ { initialTileIndex:12, type:'car04', }, { initialTileIndex:2, type:'car08', }, { initialTileIndex:-2, type:'car01', }, ], },]地形生成而铺设路面的函数就较为简单,就是将传入的mesh在scene中排成一排,随后根据当前行数为metadata中的所属行的数组下标对其位置在进行调整。
export defaultclassGrass{constructor(scene, object3d, rowIndex =0) { this.scene = scene this.object3d = object3d this.rowIndex = rowIndex this.tiles = [] this.createGrassRow() }
// 生成一行草地 createGrassRow() { // 获取 tile 资源(假设资源名为 'grass',如有不同请调整) consttileResource =this.object3d tileResource.scene.updateMatrixWorld() if(!tileResource) { console.warn('未找到 grass 资源') return } // 生成16个连续的草地瓦片 for(let i =0; i 16; i++) { // 计算当前tile的地图下标 consttileIndex = MIN_TILE_INDEX + i // 克隆tile模型 consttileMesh = tileResource.scene.clone() // 设置tile在世界坐标中的位置 tileMesh.position.set(tileIndex,0,this.rowIndex) // 将 tileMesh 沿着 X 轴排成一排,随后根据 this.rowIndex 调整 Z 轴位置 // 添加到场景 this.scene.add(tileMesh) // 存储tile对象 this.tiles.push(tileMesh) } }}路面行生成也是同理,这里就不反复贴类似功能的代码了。随后在场景中根据metadata生成对应类实例
this.metadata.forEach((rowData) = { this.rowIndex++ // 如果是森林行,添加树 if(rowData && rowData.type ==='forest') { // 先生成草地 this.addGrassRow(this.rowIndex) } if(rowData && rowData.type ==='road') { this.addRoadRow(this.rowIndex) }})
// 添加一行草地 addGrassRow(rowIndex =0) { constgrass = new Grass(this.scene,this.resources.items.grass, rowIndex) this.grassRows.push(grass) this.tiles.push(...grass.tiles) }
// 添加一行道路 addRoadRow(rowIndex =0) { constroad = new Road(this.scene,this.resources, rowIndex) this.roadRows.push(road) }我们就能得到场景如图 (行上的数字对应了当前行对应的 TileIndex)
动态元素生成现在我们需要向森林行和道路行上添加对应的物体,这些物体并不是固定的某一行有多少多少个,而是根据在metadata相对应的物体数组决定,森林行根据tree数组添加对应的树木,道路类根据vehicles添加对应的车辆。
树木生成就比如树木数组
trees: [ { tileIndex: -7, type:'tree01'}, { tileIndex: -3, type:'tree02'}, ],他就分别代表
模型名为tree01的树木模型在tileIndex位置为 -7 的位置。模型名为tree02的树木模型在tileIndex位置为 -3 的位置。(ps: 我对单个路面块再建模软件中进行过预处理,确保他们引入后长度大小刚好为 1m,所以后续tileIndex会和position的 X 轴对应)
export defaultclassTree{/** *@param{THREE.Scene} scene - threejs场景 *@param{object} resources - 资源加载器实例 *@param{Array} trees - 当前行的树木数组,每项包含tileIndex和type *@param{number} rowIndex - 当前行的z坐标 */constructor(scene, resources, trees, rowIndex =0) { this.scene = scene this.resources = resources this.trees = trees this.rowIndex = rowIndex this.treeMeshes = [] this.addTrees() }
// 添加所有树木到当前行 addTrees() { this.trees.forEach((treeData) = { const{ tileIndex, type } = treeData // 获取对应类型的树模型 consttreeResource =this.resources.items[type] // 克隆树模型 consttreeMesh = treeResource.scene.clone() // 设置树的位置(x轴为tileIndex,z轴为rowIndex) treeMesh.position.set(tileIndex,0.2,this.rowIndex) // 添加到场景 this.scene.add(treeMesh) // 存储树对象,便于后续移除 this.treeMeshes.push(treeMesh) }) }}车辆生成车辆类相比树木类需要多一层 “调整车辆方向逻辑”,这不仅需要代码配合,还需要对静态资源进行预处理,确保所有车辆朝向一致。
{ type:'road', direction:true, speed: 1, vehicles: [ { initialTileIndex: 12, type:'car04', }, { initialTileIndex: 2, type:'car08', }, { initialTileIndex: -2, type:'car01', }, ], },export defaultclassCar{/** *@param{THREE.Scene} scene - threejs场景 *@param{object} resources - 资源加载器实例 *@param{Array} vehicles - 当前行的车辆数组,每项包含 initialTileIndex 和 type *@param{number} rowIndex - 当前行的z坐标 *@param{boolean} direction - 车辆方向,true 向右,false 向左 *@param{number} speed - 车辆速度 */constructor(scene, resources, vehicles, rowIndex =0, direction =false, speed =1) { this.experience = new Experience() this.scene = scene this.resources = resources this.time =this.experience.time this.vehicles = vehicles this.rowIndex = rowIndex this.direction = direction this.speed = speed this.timeMultiplier =1 this.carMeshes = [] this.addCars() }
// 添加所有车辆到当前行 addCars() { this.vehicles.forEach((carData, _idx) = { const{ initialTileIndex, type } = carData // 获取对应类型的车辆模型 constcarResource =this.resources.items[type] if(!carResource) { console.warn(`未找到资源: ${type}`) return } // 克隆车辆模型 constcarMesh = carResource.scene.clone() carMesh.scale.set(0.5,0.5,0.5) // 递归设置所有 mesh 可投射阴影 carMesh.traverse((child) = { if(child.isMesh) { child.castShadow =true// 车辆产生阴影 } }) // 设置车辆位置(x轴为tileIndex*4,z轴为rowIndex) carMesh.position.set(initialTileIndex,0.35,this.rowIndex) // 设置车辆朝向 if(this.direction) { carMesh.rotation.y =0// 向右 } else{ carMesh.rotation.y = Math.PI// 向左 } // 添加到场景 this.scene.add(carMesh) // 存储车辆对象,便于后续移除和动画 this.carMeshes.push(carMesh) }) }}场景组装搭建随后在前面遍历metadata的地方将treecar的生成函数以同样方式调用
this.metadata.forEach((rowData) = { this.rowIndex++ // 如果是森林行,添加树 if(rowData && rowData.type ==='forest') { // 先生成草地 this.addGrassRow(this.rowIndex) this.addTreeRow(rowData.trees,this.rowIndex) } if(rowData && rowData.type ==='road') { this.addRoadRow(this.rowIndex) this.addCarRow(rowData.vehicles,this.rowIndex, rowData.direction, rowData.speed) } }) }
// 添加一行树 addTreeRow(trees, rowIndex) { consttreeRow = new Tree(this.scene,this.resources, trees, rowIndex) this.treeRows.push(treeRow) }
// 添加一行车辆 addCarRow(vehicles, rowIndex =0, direction =false, speed =1) { constcarRow = new Car(this.scene,this.resources, vehicles, rowIndex, direction, speed) this.carRows.push(carRow) // 新增:记录每行车辆mesh this.carMeshDict[rowIndex] = carRow.getCarMeshes() }最后我们只需要在给车辆增加移动效果,让车辆随着requestAnimationFrame更新不断更新mesh的位移, 向指定方向direction移动即可。记得别忘了超出边界要及时重置车辆位置哦。
// 更新车辆位置(可用于动画) update() { // 获取全局已用时间,单位ms,转为秒 constt =this.time.elapsed *0.03*this.timeMultiplier this.carMeshes.forEach((car, idx) = { // 车辆移动方向 constdir =this.direction ?1: -1 // 边界判断与循环 重置车辆位置 if(dir ===1&& car.position.x CAR_BOUNDARY_MAX) { car.position.x = CAR_BOUNDARY_MIN } elseif(dir === -1&& car.position.x CAR_BOUNDARY_MIN) { car.position.x = CAR_BOUNDARY_MAX }
car.position.x += dir *this.speed *this.time.delta *1/60*0.23*this.timeMultiplier
// === 车身抖动:模拟不平路面 === // 抖动参数 constshake =this.carShakeParams[idx] // 叠加两组不同频率的正弦波,幅度小 constfreq1 =2.5 constamp1 =0.02 constfreq2 =4.3 constamp2 =0.01 // 计算抖动偏移 constoffsetY = Math.sin(t * freq1 + shake.phase) * amp1 + Math.cos(t * freq2 + shake.phase *1.3) * amp2 car.position.y = shake.baseY + offsetY }) }(最后效果图)
这个我们就已经完成了根据metadata生成对应的场景内容的功能,现在您可以为用户生成一个较为简单的初始场景,虽然我们后面会利用随机生成的metadata数据生成地形,但相信我,我曾经生成过除去出生点后面 6、7 行都是马路。毕竟玩家刚复活下一步就要开始过马路不是一个很好的游戏体验。
5.??角色移动与场景生成引入角色首先,我们需要将小鸡模型添加到场景中并进行适当调整:
// 加载并放置小鸡模型 initChicken() { // 获取 instance 资源 constchickenResource =this.resources.items.bigChicken if(!chickenResource) { console.warn('未找到 instance 资源') return } // 克隆模型,避免资源污染 this.instance = chickenResource.scene.clone() // 显示阴影 this.instance.traverse((child) = { if(child instanceof THREE.Mesh) { child.castShadow =true } }) // 只设置 y 方向初始高度 this.instance.position.set(0,0.22,0) // 设置初始等比例缩放 this.instance.scale.set(this.scale,this.scale,this.scale) // 添加到 agentGroup this.agentGroup.add(this.instance) }角色移动随后需要对小鸡加入按键监听以及移动控制,在之前的项目?????? 我写出了被 Threejs 官推转发的项目????!中,我们已经探讨过角色移动的基本原理。这个项目我沿用了当时的角色按键移动响应和转向逻辑,在这里就不炒冷饭去解释_为什么使用 event.code_ 或者_角色转向 BUG_。「直接重点解释当前项目和上一个项目移动的不同点:引入入了移动队列 (movesQueue) 机制」
首先我们来看在游戏中正常移动是什么样的。
可以看到小鸡在场景中每次都是「每次按键都对应一个完整的移动单位,不会因按键时间长短影响移动距离」。这就是为什么我们相较于前作的移动逻辑多了一个movesQueue,movesQueue的作用是作为移动指令队列,存储待执行的移动方向(如 forward、left)。用户可以连续输入多个移动指令,使动画未完成,用户的后续输入也会被记录。避免了突然变向等不自然现象。
以下是this.movesQueue的监听逻辑
listenKeyboard() { window.addEventListener('keydown',(event) ={ if(this.experience.isPaused) { return } letmove =null switch(event.code) { case'ArrowUp': case'KeyW': move ='forward' break case'ArrowDown': case'KeyS': move ='backward' break case'ArrowLeft': case'KeyA': move ='left' break case'ArrowRight': case'KeyD': move ='right' break default: break } // 只在首次按下时 push if(move && !this.pressedKeys.has(event.code)) { this.movesQueue.push(move) this.pressedKeys.add(event.code) } }) window.addEventListener('keyup',(event) ={ this.pressedKeys.delete(event.code) }) }随后this.movesQueue会在requestAnimationFrame中进行相应指令的执行和释放。这里有一个具体的流程图如下:
(移动流程图)
核心逻辑可简化为三个步骤:
根据指令计算目标格子执行动画插值移除已完成的指令以下是核心代码片段
update() { if(!this.instance) return if(!this.movesQueue.length) return
// 计算下一步目标格子 if(!this.isMoving) { constdir =this.movesQueue[0] this.targetTile = { ...this.currentTile } switch (dir) { case'forward': this.targetTile.z -=1 break case'backward': this.targetTile.z +=1 break case'left': this.targetTile.x -=1 break case'right': this.targetTile.x +=1 break }
// 先设置旋转,让小鸡朝向尝试方向 this.startRot =this.instance.rotation.y this.endRot = getTargetRotation(dir)
// 启动移动 this.isMoving =true this.moveClock.start() // 记录起始位置 this.startPos = { x:this.currentTile.x *this.stepLength, z:this.currentTile.z *this.stepLength, } this.endPos = { x:this.targetTile.x *this.stepLength, z:this.targetTile.z *this.stepLength, } }
// 步进动画 conststepTime =this.isSpeedUp ? SPEEDUP_STEP_TIME : NORMAL_STEP_TIME// 根据加速状态调整步进时长 constprogress = Math.min(1,this.moveClock.getElapsedTime() / stepTime) this.setPosition(progress) this.setRotation(progress)
// 步进结束 if(progress =1) { this.stepCompleted() this.moveClock.stop() this.isMoving =false // 移除已完成的指令 this.movesQueue.shift() } }角色现在是可以移动了,但是当玩家移动到场景边界时会出现...
下面我们需要解决场景生成问题?不能一次生成太多,也不能让用户看到场景边界!
场景生成我们需要实现动态地形生成来解决这个问题,主要考虑两个关键点:
「生成时机」:何时触发地形扩展?「生成方式」:如何生成新的地形?生成时机判定让我们来看一张图
这里我将「刷新时机可以被认定为当用户距离地图板边小于一定距离时触发」,用户的位置我们很容易获取到,而地图板边呢?
我们可以将this.metadata.length作为地图板边距离,毕竟在scene里,一个row的Z轴长度就为1
// 检查玩家距离地图末尾距离,自动扩展checkAndExtendMap(userZ) { // userZ 为玩家当前 z 坐标(负数,越小越远) constremainRows =this.metadata.length-Math.abs(userZ)//距离板边的距离 if(remainRows GENERATION_COUNT) { //TODO:扩充地形 } }地形生成地形生成逻辑如下,而对于generateMetaRows的逻辑我不想过多赘述,使用AI将现有的metadata数据贴入上下文,大概半分钟就能生成这样一个函数。
// 扩展地图,生成并渲染 N 个新行 extendMap(N =10) { conststartRowIndex =this.metadata.length constnewRows = generateMetaRows( N) this.metadata.push(...newRows)
// 渲染新行 newRows.forEach((rowData) = { this.rowIndex++ if(rowData.type ==='forest') { this.addGrassRow(this.rowIndex) this.addTreeRow(rowData.trees,this.rowIndex) } if(rowData.type ==='road') { this.addRoadRow(this.rowIndex) this.addCarRow(rowData.vehicles,this.rowIndex, rowData.direction, rowData.speed) } }) }
// 检查玩家距离地图末尾距离,自动扩展 checkAndExtendMap(userZ) { // userZ 为玩家当前 z 坐标(负数,越小越远) constremainRows =this.metadata.length - Math.abs(userZ) if(remainRows GENERATION_COUNT) { this.extendMap(GENERATION_COUNT) } }最后在让我们进入游戏中试试
(这里特地把视角调远)
6.??用户碰撞检测当前问题:无所不能的小鸡!!目前游戏完全没有碰撞检测,玩家可以随心所欲地穿过任何物体:
我只能说小鸡是懂刷分这一块的!
树木碰撞检测树木碰撞其实要比想象中的要简单,还记得在角色移动前我们会计算下一个目标格得到一个this.targetTile吗?我们可以根据this.targetTile在metadata相应位置上是否存在tree而判断是否触发了树木碰撞。那么此时原先流程图的相应部分现在变成了
/*** 判断目标格子是否为有效位置*@param{{x:number, z:number}} targetTile 目标格子坐标*@param{Array} metaData 地图元数据数组*@returns{boolean} 是否为有效位置*/export function endsUpInValidPosition(targetTile, metaData) {// 1. 边界检查if(targetTile.x MIN_TILE_INDEX || targetTile.x MAX_TILE_INDEX) returnfalseif(targetTile.z = -5) returnfalse
// 2. 检查 metaData 是否有树constrowIndex = targetTile.zconstrow = metaData[rowIndex -1]if(row && row.type ==='forest') { // 检查该行是否有树在目标 x if(row.trees.some(tree = tree.tileIndex === targetTile.x)) { returnfalse } }returntrue}
// update方法:每帧调用,处理移动逻辑 update() { if(!this.instance) return if(!this.movesQueue.length) return
// 计算下一步目标格子 if(!this.isMoving) { constdir =this.movesQueue[0] constnextTarget = { ...this.currentTile } switch (dir) { case'forward': nextTarget.z -=1 break case'backward': nextTarget.z +=1 break case'left': nextTarget.x -=1 break case'right': nextTarget.x +=1 break }
// 检查是否合法 constmapMetadata =this.experience.world.map.metadata if(!endsUpInValidPosition(nextTarget, mapMetadata)) {// 不合法则丢弃指令 this.movesQueue.shift() return }//后续逻辑不变... }增强碰撞反馈但是这样碰撞虽然发生,但给人一点反馈没有,为了让碰撞更有感觉,我们增加了两个效果:
虽然撞墙但也转向行为原地蹦跶两下意思意思行为 if(!endsUpInValidPosition(nextTarget, mapMetadata)) { this.setRotation(1)// 虽然撞墙但也转向行为 // 不合法,执行 yoyo 动画并丢弃本次指令 this.playYoyoAnimation(nextTarget)//原地蹦跶两下意思意思行为 this.movesQueue.shift()原地蹦跶两下意思意思行为 return }this.playYoyoAnimation效果类似gsap的yoyo行为,跳向目标点随后返回原tile。
最终效果如下:
汽车碰撞那么汽车碰撞是怎么实现的呢?毕竟场景里动态元素这么多,使用octree肯定不行。我这里使用了BOX3为车辆和用户创建了包围盒,随后判断是否车辆和用户相交进而确定游戏进程是否继续。
AABB?!这场景这么多车你用这方法?这不是越玩越卡,开头120FPS,结尾卡成PPT。可是。。。。为什么你的电脑这么流畅,难道你用4090来玩这个4399小游戏?
「空间分区」:只检测玩家当前所在道路行的车辆答案是只对玩家当前所在的道路行进行碰撞测试,比如玩家 A 在道路行 31 行,那么只要求第 31 行的车辆生成包围盒并判断是否与用户相交。
那么首先我们需要解决的问题是 如何获取玩家当前所在行的所有汽车mesh, 这就需要我们提前维护一个汽车快表, 将每个行的行数以及当前汽车 mesh 映射起来
这里我们回到「车辆生成」相关代码,在这里维护一个this.carMeshDict字典
// 新增:行号到车辆mesh数组的映射 this.carMeshDict= {}
// 初始化地图内容initializeMap() {//....逻辑不变 }
// 添加一行车辆addCarRow(vehicles, rowIndex =0, direction =false, speed =1) { constcarRow =newCar(this.scene,this.resources, vehicles, rowIndex, direction, speed) this.carRows.push(carRow) // 新增:记录每行车辆mesh this.carMeshDict[rowIndex] = carRow.getCarMeshes() }随后封装一个方法来帮助快速获取某行车辆, 并解决判空问题。
// 新增:获取指定行的车辆mesh数组getCarMeshesByRow(rowIndex) { returnthis.carMeshDict[rowIndex] || [] }最后遍历每辆汽车,逐一检测碰撞
对每个汽车 Mesh,分别构建包围盒(THREE.Box3)。
构建玩家的包围盒。
使用 Box3.intersectsBox() 判断玩家和汽车是否有包围盒重叠(即发生碰撞)。
update() { // 如果游戏已结束,直接返回,防止继续执行 update 逻辑 if(this.map) { this.map.update() if(this.user && !this.isGameOver) { this.map.checkAndExtendMap(this.user.currentTile.z) // === 碰撞检测 === // 获取玩家mesh和所在行 constplayerMesh =this.user.instance if(playerMesh) { constplayerRow =this.user.currentTile.z constcarMeshes =this.map.getCarMeshesByRow(playerRow) if(carMeshes.length 0) { // 构建玩家包围盒 constplayerBox = new THREE.Box3().setFromObject(playerMesh) for(constcarMesh of carMeshes) { constcarBox = new THREE.Box3().setFromObject(carMesh) if(playerBox.intersectsBox(carBox)) { this.onGameOver()//撰写你想要的结束效果 } } } } this.user.update() } } }(顺便在这贴上`BOX3`资源释放相关帖[19])
简单的游戏结束效果包括:
玩家位置重置显示结束 UI上传分数这种实现方式既保证了碰撞检测的准确性,又通过空间分区优化确保了游戏性能,即使在低端设备上也能流畅运行。
7.??GAME UI 通信在撰写Threejs项目时,我们常常面临一个关键挑战:如何将 3D 场景 (Scene) 中的动态信息有效地传递到 2D 游戏界面(Game UI)。就比如在这个游戏中当用户获取特殊道具时,虽然放慢周围车辆的特效很炫酷,但如果缺少清晰的道具倒计时提示和氛围光晕效果,整个体验就会显得不够完整。
框架解决方案的利与弊目前市场上已有React Three Fiber和Tresjs等响应式框架,它们通过内置机制简化了3D场景与UI之间的数据交互 (类似的 issue)。这些框架确实提供了便捷的解决方案。然而,框架学习本身需要时间成本,且可能带来项目依赖性的问题。如果用户是在一个老的3D项目上维护场景呢?
回归本质:事件驱动架构当我们希望保持项目轻量级,或者需要更灵活的解决方案时,可以回归到事件驱动架构(风水轮流转了属于是,没准前端真的是个圈)。这种发布 - 订阅模式 (Pub-Sub) 完美适用于处理事件触发和状态变更的场景:
首先让我们看threejs的入口文件Experience实体类,他负责接受页面canvas元素,随后将Render挂载到对应canvas上,这里我们让 Experience 类继承自 EventEmitter(事件发射器),具备事件注册、触发、移除等能力。
组件 / 模块通过 on('事件名', 回调) 订阅事件。
其他地方通过 trigger('事件名', [参数]) 触发事件,所有订阅者收到通知。
import*asTHREE from'three'
importCamera from'./camera.js'importRenderer from'./renderer.js'importsources from'./sources.js'importDebug from'./utils/debug.js'importEventEmitter from'./utils/event-emitter.js'importIMouse from'./utils/imouse.js'importResources from'./utils/resources.js'importSizes from'./utils/sizes.js'importStats from'./utils/stats.js'importTime from'./utils/time.js'importPhysicsWorld from'./world/physics-world.js'importWorld from'./world/world.js'
let instance
export defaultclassExperienceextendsEventEmitter{constructor(canvas) { // 确保单一实例 if(instance) { returninstance }
super() instance =this
// Global access window.Experience =this
this.canvas = canvas
// 实例化所有类组件 this.debug = new Debug() this.stats = new Stats() this.sizes = new Sizes() this.time = new Time() this.scene = new THREE.Scene() this.camera = new Camera(true) this.renderer = new Renderer() this.resources = new Resources(sources) this.physics = new PhysicsWorld() this.iMouse = new IMouse() this.world = new World()
this.sizes.on('resize', () = { this.resize() })
this.time.on('tick', () = { this.update() })
// 事件监听测试 this.on('pause', () = { this.isPaused =true// 设置为暂停 }) this.on('resume', () = { this.isPaused =false }) }
resize() { this.camera.resize() this.renderer.resize() }
update() { if(this.isPaused) return this.camera.update() this.world.update() this.renderer.update() this.stats.update() this.iMouse.update() }}页面 UI 操控 游戏场景完成threejs项目入口文件继承了发布订阅类之后,我们就能在获取唯一实例并调用trigger&on方法来进行Threejs scenegame UI之间的信息传输
游戏场景操作页面 UI8.??朋友,端午节快乐!六一儿童节快乐!随着最后一个commit被push到远端,这个承载着心意的小项目安静地躺在 GitHub 上。还记得第一次敲下前端代码时的纯粹吗?那时的我,不在乎 deadline 的催促,不关心 KPI 的考核,只想要创造一个能让人眼前一亮的页面。
是时候将他分享出去了!
项目的开源方便给你的小孩、身边朋友、所爱之人埋下彩蛋,我只希望这个游戏能带来单纯的快乐,所以右上角的排行榜会在每天凌晨刷新,弱化他的竞技性。Work Life Balance不只是口号,这个小游戏就是我的践行方式。愿这些跳动的代码能为你和所爱之人带来欢乐!「各位掘友,端午节和六一儿童节 节日快乐」!
愿我们永远保持对生活的热爱,对代码的激情,就像第一次写出 "Hello World" 时那样满怀欣喜。
(小提示:试着在游戏里寻找隐藏的粽子图案哦~)
9.最后的一些话技术的未来与前端迁移随着 AI 技术的快速发展,各类技术的门槛正在大幅降低,以往被视为高门槛的3D技术也不例外。与此同时,过去困扰开发者的数字资产构建成本问题,也正在被最新的3D generation技术所攻克。这意味着,在不久的将来,前端开发将迎来一次技术迁移,开发者需要掌握更新颖的交互方式和更出色的视觉效果。
本专栏的愿景本专栏的愿景是通过分享Three.js的中高级应用和实战技巧,帮助开发者更好地将3D技术应用到实际项目中,打造令人印象深刻的Hero Section。我们希望通过本专栏的内容,能够激发开发者的创造力,推动Web3D技术的普及和应用。
加入掘金社区,共同成长如果您对Threejs这个3D图像框架很感兴趣,或者您也深信未来国内会涌现越来越多3D设计风格的网站,欢迎加入「稀土掘金社区」。
此外,如果您很喜欢Threejs又在烦恼其原生开发的繁琐,那么我诚邀您尝试「Tresjs」和「TvTjs」, 他们都是基于Vue的Threejs框架。「TvTjs」也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源!
AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding
点击"阅读原文"了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线