诽闹级瓤项目背景最近在研究 Rokid AR 眼镜的开发想做点有意思的东西。看了一圈案例发现大家都在做一些比较正经的应用——导航、信息展示之类的。我就想能不能整点不一样的游戏而且是用语音控制的游戏。这篇文章记录了我从零开始用 JSAR 框架开发一款太空射击小游戏的全过程。代码不复杂效果还挺酷的。image技术选型框架: JSAR (Rokid 官方的空间应用运行时)3D 引擎: Babylon.js目标设备: Rokid AR 智能眼镜开发环境: VSCode JSAR 扩展游戏设计在写代码之前先把游戏玩法想清楚元素设计玩家角色三角形飞船可左右移动敌人随机生成的陨石从上往下掉战斗方式发射能量弹击毁陨石得分规则每击毁一个陨石10分失败条件被陨石撞击3次游戏结束难度曲线分数越高陨石速度越快控制方式设计成双模式键盘模式电脑上调试用方向键移动空格射击语音模式Rokid 眼镜实际使用说左、右、发射核心技术实现1. 3D 场景搭建1.1 相机视角采用 45 度俯视角让玩家能同时看到自己的飞船和前方飞来的陨石const camera new BABYLON.ArcRotateCamera(camera,0,Math.PI / 4, // 45度俯视角80, // 距离场景中心80单位new BABYLON.Vector3(0, 0, 0),scene);1.2 网格地板为了增强 3D 空间感添加了蓝色网格地板// 用线条绘制网格const gridSize 150;const gridDivisions 15;const lineColor new BABYLON.Color3(0, 0.5, 1);// 横向线for (let i 0; i gridDivisions; i) {const z (i / gridDivisions) * 200;const points [new BABYLON.Vector3(-gridSize / 2, -20, z - 50),new BABYLON.Vector3(gridSize / 2, -20, z - 50)];const line BABYLON.MeshBuilder.CreateLines(gridLineH i, { points }, scene);line.color lineColor;line.alpha 0.5;}// 纵向线类似代码网格的透视效果让玩家能清晰感知深度和速度。image1.3 星空背景用点云系统创建 300 颗随机分布的星星const starCount 300;const stars new BABYLON.PointsCloudSystem(stars, 3, scene);stars.addPoints(starCount, (particle, i) {particle.position new BABYLON.Vector3(Math.random() * 200 - 100,Math.random() * 200 - 100,Math.random() * 100 30 // Z轴分布增强深度);particle.color new BABYLON.Color4(1, 1, 1, Math.random() * 0.8 0.2);});stars.buildMeshAsync();image2. 飞船系统2.1 飞船模型飞船由三角形机身 机翼 引擎光效组成。为了在 AR 眼镜中看得清楚所有尺寸都放大了 5 倍// 三角形机身const ship BABYLON.MeshBuilder.CreateCylinder(player, {height: 10, // 放大5倍后的尺寸diameterTop: 0,diameterBottom: 7.5,tessellation: 3 // 三条边形成三角形}, scene);ship.rotation.x Math.PI / 2; // 旋转90度让尖端朝上ship.position.y -15;// 添加机翼增强3D效果const wingLeft BABYLON.MeshBuilder.CreateBox(wingLeft, {width: 3,height: 0.5,depth: 4}, scene);wingLeft.position new BABYLON.Vector3(-4, -2, 0);wingLeft.parent ship;// 引擎光效const engineGlow BABYLON.MeshBuilder.CreateSphere(engineGlow, {diameter: 2.5}, scene);engineGlow.position new BABYLON.Vector3(0, -6, 0);engineGlow.parent ship;const glowMaterial new BABYLON.StandardMaterial(glowMat, scene);glowMaterial.emissiveColor new BABYLON.Color3(1, 0.5, 0); // 橙色发光engineGlow.material glowMaterial;2.2 流畅移动控制使用按键状态追踪实现流畅的连续移动this.keys {};window.addEventListener(keydown, (event) {this.keys[event.key] true;if (event.key || event.key Enter) {this.shoot();}});window.addEventListener(keyup, (event) {this.keys[event.key] false;});// 在渲染循环中处理移动scene.registerBeforeRender(() {if (this.keys[ArrowLeft] || this.keys[a] || this.keys[A]) {this.movePlayer(-1);}if (this.keys[ArrowRight] || this.keys[d] || this.keys[D]) {this.movePlayer(1);}});这样按住方向键就能持续移动没有系统按键延迟的顿挫感。image3. 武器系统3.1 子弹设计子弹采用圆柱体造型带有尾迹效果shoot() {const bullet BABYLON.MeshBuilder.CreateCylinder(bullet, {height: 3,diameter: 1}, this.scene);bullet.position this.player.position.clone();bullet.position.y 5;bullet.rotation.x Math.PI / 2;// 黄色发光材质const material new BABYLON.StandardMaterial(bulletMat, this.scene);material.emissiveColor new BABYLON.Color3(1, 1, 0);bullet.material material;// 添加尾迹const trail BABYLON.MeshBuilder.CreateCylinder(trail, {height: 2,diameter: 0.5}, this.scene);trail.position new BABYLON.Vector3(0, -2.5, 0);trail.parent bullet;const trailMat new BABYLON.StandardMaterial(trailMat, this.scene);trailMat.emissiveColor new BABYLON.Color3(1, 0.8, 0);trailMat.alpha 0.6;trail.material trailMat;this.bullets.push(bullet);}image3.2 子弹更新逻辑scene.registerBeforeRender(() {for (let i this.bullets.length - 1; i 0; i--) {this.bullets[i].position.y 8; // 速度也放大了// 飞出屏幕后销毁if (this.bullets[i].position.y 50) {this.bullets[i].dispose();this.bullets.splice(i, 1);}}});4. 敌人系统4.1 陨石生成陨石使用 Babylon.js 的多面体有 3 种不同形状spawnAsteroid() {const asteroid BABYLON.MeshBuilder.CreatePolyhedron(asteroid, {type: Math.floor(Math.random() * 3), // 0-2随机形状size: Math.random() * 4 3 // 放大5倍}, this.scene);asteroid.position new BABYLON.Vector3(Math.random() * 80 - 40, // X: -40到40随机位置50, // Y: 从顶部出现Math.random() * 10 - 5 // Z: -5到5增加深度变化);// 岩石材质const material new BABYLON.StandardMaterial(asteroidMat, this.scene);material.diffuseColor new BABYLON.Color3(0.6, 0.4, 0.3);material.emissiveColor new BABYLON.Color3(0.2, 0.1, 0.05);asteroid.material material;// 随机旋转速度asteroid.rotationSpeed new BABYLON.Vector3(Math.random() * 0.05,Math.random() * 0.05,Math.random() * 0.05);this.asteroids.push(asteroid);}image4.2 定时生成startAsteroidSpawner() {this.asteroidSpawnTimer setInterval(() {this.spawnAsteroid();}, 2000); // 每2秒生成一个}5. 碰撞检测与爆炸效果5.1 碰撞检测使用 Babylon.js 内置的 intersectsMesh 方法for (let i this.asteroids.length - 1; i 0; i--) {const asteroid this.asteroids[i];// 检测子弹碰撞let hit false;for (let j this.bullets.length - 1; j 0; j--) {const bullet this.bullets[j];if (asteroid.intersectsMesh(bullet, false)) {this.createExplosion(asteroid.position);asteroid.dispose();bullet.dispose();this.asteroids.splice(i, 1);this.bullets.splice(j, 1);this.score 10;this.updateUI();hit true;break;}}// 检测飞船碰撞if (!hit asteroid.intersectsMesh(this.player, false)) {this.createExplosion(asteroid.position);asteroid.dispose();this.asteroids.splice(i, 1);this.lives--;this.updateUI();if (this.lives 0) {this.endGame();}}}5.2 粒子爆炸效果createExplosion(position) {const particleSystem new BABYLON.ParticleSystem(explosion, 100, this.scene);particleSystem.particleTexture new BABYLON.Texture(, this.scene);particleSystem.emitter position;particleSystem.minSize 0.5;particleSystem.maxSize 1.5;particleSystem.minLifeTime 0.3;particleSystem.maxLifeTime 0.6;particleSystem.emitRate 200;particleSystem.createSphereEmitter(1);particleSystem.color1 new BABYLON.Color4(1, 0.5, 0, 1);particleSystem.color2 new BABYLON.Color4(1, 0.8, 0, 1);particleSystem.colorDead new BABYLON.Color4(0.5, 0.5, 0.5, 0);particleSystem.minEmitPower 1;particleSystem.maxEmitPower 3;particleSystem.start();setTimeout(() {particleSystem.stop();setTimeout(() particleSystem.dispose(), 1000);}, 200);}6. UI 系统6.1 得分和生命值显示UI 使用 DynamicTexture 在 3D 平面上绘制文字createUI() {// 得分显示this.scoreText BABYLON.MeshBuilder.CreatePlane(scoreText, {width: 60,height: 12}, this.scene);this.scoreText.position new BABYLON.Vector3(-50, 40, 0);const scoreTexture new BABYLON.DynamicTexture(scoreTexture,{ width: 1024, height: 256 }, this.scene, true);const scoreMaterial new BABYLON.StandardMaterial(scoreMat, this.scene);scoreMaterial.diffuseTexture scoreTexture;scoreMaterial.emissiveColor new BABYLON.Color3(1, 1, 1);this.scoreText.material scoreMaterial;this.scoreText.renderingGroupId 1; // 确保UI在最上层this.scoreTexture scoreTexture;// 生命值显示类似代码}updateUI() {// 更新得分const ctx this.scoreTexture.getContext();ctx.clearRect(0, 0, 1024, 256);ctx.fillStyle #FFD700;ctx.font bold 120px Arial;ctx.textAlign left;ctx.fillText(得分: this.score, 50, 160);this.scoreTexture.update();// 更新生命值类似代码}canvas 尺寸是 1024x256而 plane 是 60x12这样宽高比一致文字不会变形。image6.2 游戏结束界面endGame() {this.gameOver true;clearInterval(this.asteroidSpawnTimer);const gameOverText BABYLON.MeshBuilder.CreatePlane(gameOverText, {width: 120,height: 40}, this.scene);gameOverText.position new BABYLON.Vector3(0, 0, 0);const texture new BABYLON.DynamicTexture(gameOverTexture, {width: 1024,height: 512}, this.scene, true);const ctx texture.getContext();ctx.fillStyle rgba(0, 0, 0, 0.9);ctx.fillRect(0, 0, 1024, 512);ctx.fillStyle #FF3333;ctx.font bold 100px Arial;ctx.textAlign center;ctx.fillText(游戏结束, 512, 150);ctx.fillStyle #FFD700;ctx.font bold 80px Arial;ctx.fillText(最终得分: this.score, 512, 280);ctx.fillStyle #FFFFFF;ctx.font 50px Arial;ctx.fillText(按 R 重新开始, 512, 400);texture.update();const material new BABYLON.StandardMaterial(gameOverMat, this.scene);material.diffuseTexture texture;material.emissiveColor new BABYLON.Color3(0.8, 0.8, 0.8);gameOverText.material material;gameOverText.renderingGroupId 2; // 最上层}7. 语音控制7.1 Rokid 语音 APIJSAR 提供了 rokid.voice API 用于语音识别setupVoiceControl() {if (typeof rokid ! undefined rokid.voice) {rokid.voice.on(command, (command) {switch(command) {case 左:this.movePlayer(-1);break;case 右:this.movePlayer(1);break;case 发射:case 开火:this.shoot();break;}});}}7.2 键盘模拟语音调试用在电脑上调试时用 J/K/L 键模拟语音命令window.addEventListener(keydown, (event) {// 语音模拟if (event.key j || event.key J) {console.log(?? 语音命令: 左);this.movePlayer(-1);} else if (event.key l || event.key L) {console.log(?? 语音命令: 右);this.movePlayer(1);} else if (event.key k || event.key K) {console.log(?? 语音命令: 发射);this.shoot();}});image8. 游戏难度递增根据得分提升游戏难度// 在碰撞检测中击中陨石后this.score 10;this.updateUI();// 难度递增if (this.score % 50 0 this.difficulty 2) {this.difficulty 0.1;console.log(难度提升至: this.difficulty.toFixed(1));}// 在陨石更新时应用难度asteroid.position.y - GAME_CONFIG.asteroidSpeed * this.difficulty;每得 50 分陨石速度提升 10%最高提升到 2 倍速。性能优化对象管理游戏中会频繁创建和销毁子弹、陨石必须及时清理// 子弹飞出屏幕if (bullet.position.y 50) {bullet.dispose(); // 销毁3D对象this.bullets.splice(i, 1); // 从数组删除}// 陨石飞出屏幕if (asteroid.position.y -40) {asteroid.dispose();this.asteroids.splice(i, 1);}渲染分组使用 renderingGroupId 控制渲染顺序确保 UI 始终在最上层this.scoreText.renderingGroupId 1;this.livesText.renderingGroupId 1;gameOverText.renderingGroupId 2;控制说明键盘模式电脑调试按键功能← / A向左移动→ / D向右移动空格 / 回车发射R重新开始语音模拟键盘按键模拟语音J左L右K发射语音控制Rokid 眼镜直接说左 - 飞船左移右 - 飞船右移发射 / 开火 - 射击开发心得踩过的坑忘记 dispose 对象 → 内存泄漏游戏越来越卡碰撞检测死循环 → 删除元素时索引出错要倒序遍历粒子系统没清理 → 爆炸效果叠加导致卡顿相机角度调试 → 最初是正面视角太平面改成 45 度俯视后立体感大增物体尺寸太小 → 第一版看不清细节放大 5-10 倍后体验好很多经验总结3D 游戏要注重空间感相机角度、参考物网格、Z 轴变化尺寸和速度要成比例调整放大物体后速度也要相应提升按键状态追踪比 keydown 事件更流畅没有系统延迟UI 宽高比canvas 和 plane 的宽高比要一致避免文字变形及时清理对象dispose 数组 splice 两步都要做多用 console.log 调试特别是碰撞检测部分