项目概述
本项目是一个基于 JSAR(JavaScript Spatial Augmented Reality)框架和 Babylon.js 引擎开发的 3D 交互游戏,专为 Rokid AR 眼镜设计。游戏采用简洁的玩法:玩家通过手指移动(本地使用鼠标模拟)控制黄色球体,移动去接触随机生成的红色目标球,每次成功接触即可得分。
技术栈
- JSAR: Rokid AR 空间计算框架
- Babylon.js: 3D 渲染引擎
- TypeScript: 开发语言
- XSML: 空间场景标记语言
场景结构与核心设计
XSML 场景定义
项目使用 XSML 作为入口文件,定义了空间场景的基本结构:
<xsml version="1.0">
<head>
<title>接球游戏</title>
<script src="./lib/main.ts"></script>
</head>
<space>
</space>
</xsml>
XSML 提供了轻量级的场景声明方式,所有游戏逻辑都在 TypeScript 模块中实现。
核心代码深度解析
1. 场景初始化
游戏采用俯视视角,创建了一个 20x20 的网格地面作为游戏区域:
// 场景设置
scene.clearColor = new BABYLON.Color4(0.1, 0.1, 0.15, 1);
// 光照
const light = new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 1.2;
// 创建地面与网格线
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: GRID_SIZE * CELL_SIZE,
height: GRID_SIZE * CELL_SIZE
}, scene);

网格线的绘制增强了空间感知,帮助玩家更好地判断位置:
for (let i = 0; i <= GRID_SIZE; i++) {
const lineH = BABYLON.MeshBuilder.CreateBox('lineH' + i, {
width: GRID_SIZE * CELL_SIZE,
height: 0.05,
depth: 0.05
}, scene);
lineH.position = new BABYLON.Vector3(0, 0.01, i * CELL_SIZE - GRID_SIZE * CELL_SIZE / 2);
// ... 垂直网格线类似
}

2. 玩家与目标对象
黄色玩家球带有发光效果和呼吸动画:
const player = BABYLON.MeshBuilder.CreateSphere('player', {
diameter: CELL_SIZE * 0.8
}, scene);
const playerMat = new BABYLON.StandardMaterial('playerMat', scene);
playerMat.diffuseColor = new BABYLON.Color3(1, 1, 0.3);
playerMat.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0); // 自发光
player.material = playerMat;
// 呼吸动画
let pulseTime = 0;
scene.onBeforeRenderObservable.add(() => {
pulseTime += 0.05;
const scale = 1 + Math.sin(pulseTime) * 0.1;
player.scaling = new BABYLON.Vector3(scale, scale, scale);
});

红色目标球随机生成在网格上:
function createTarget() {
if (target) {
target.dispose();
target = null;
}
// 随机网格坐标
targetGridX = Math.floor(Math.random() * GRID_SIZE);
targetGridY = Math.floor(Math.random() * GRID_SIZE);
target = BABYLON.MeshBuilder.CreateSphere('target_' + Date.now(), {
diameter: CELL_SIZE * 0.8
}, scene);
const targetMat = new BABYLON.StandardMaterial('targetMat_' + Date.now(), scene);
targetMat.diffuseColor = new BABYLON.Color3(1, 0.2, 0.2);
targetMat.emissiveColor = new BABYLON.Color3(0.5, 0, 0);
target.material = targetMat;
// 坐标转换:网格 -> 世界坐标
const targetX = targetGridX * CELL_SIZE - GRID_SIZE * CELL_SIZE / 2 + CELL_SIZE / 2;
const targetZ = targetGridY * CELL_SIZE - GRID_SIZE * CELL_SIZE / 2 + CELL_SIZE / 2;
target.position = new BABYLON.Vector3(targetX, CELL_SIZE / 2, targetZ);
}

3. JSAR 鼠标交互系统
这是项目的核心技术突破。JSAR 的交互系统与传统 Babylon.js 的 ActionManager 不同,需要使用专门的输入事件 API。
关键发现:必须先调用 `watchInputEvent()` 启用输入监听:
try {
(spatialDocument as any).watchInputEvent();
console.log('[OK] watchInputEvent() 调用成功');
} catch (error) {
console.log('[ERROR] watchInputEvent() 调用失败:', error);
}
动态坐标映射算法:
鼠标原始坐标需要映射到游戏网格,但不同设备的坐标范围不同。解决方案是动态追踪坐标范围:
let mouseEventCount = 0;
let minMouseX = 999999, maxMouseX = 0;
let minMouseY = 999999, maxMouseY = 0;
spatialDocument.addEventListener('mouse', (event: any) => {
if (!event.inputData) return;
const mouseX = event.inputData.PositionX;
const mouseY = event.inputData.PositionY;
// 关键:过滤无效坐标
if (mouseX === 0 || mouseY === 0) {
return;
}
// 记录坐标范围
minMouseX = Math.min(minMouseX, mouseX);
maxMouseX = Math.max(maxMouseX, mouseX);
minMouseY = Math.min(minMouseY, mouseY);
maxMouseY = Math.max(maxMouseY, mouseY);
const rangeX = maxMouseX - minMouseX;
const rangeY = maxMouseY - minMouseY;
// 只有足够的移动范围才更新位置
if (rangeX > 20 && rangeY > 20) {
const normalizedX = (mouseX - minMouseX) / rangeX;
const normalizedY = (mouseY - minMouseY) / rangeY;
playerGridX = Math.floor(normalizedX * GRID_SIZE);
playerGridY = Math.floor(normalizedY * GRID_SIZE);
// 限制在网格内
playerGridX = Math.max(0, Math.min(GRID_SIZE - 1, playerGridX));
playerGridY = Math.max(0, Math.min(GRID_SIZE - 1, playerGridY));
updatePlayerPosition();
}
});
设计亮点:
1. 过滤无效坐标:(0,0) 坐标会破坏范围计算,必须过滤
2. 动态范围适配:不依赖固定坐标范围,自动适应不同设备
3. 最小阈值检测:rangeX/Y > 20 确保有足够的移动数据才开始映射
4. 碰撞检测与得分系统
采用网格坐标精确匹配的方式检测碰撞:
if (playerGridX === targetGridX && playerGridY === targetGridY) {
score++;
console.log('[SUCCESS] 得分成功!');
console.log('当前得分: ' + score);
// 创建新目标
createTarget();
// 玩家闪烁反馈
const originalColor = playerMat.diffuseColor.clone();
playerMat.diffuseColor = new BABYLON.Color3(0, 1, 0);
setTimeout(() => {
playerMat.diffuseColor = originalColor;
}, 200);
}

游戏界面展示
俯视角度展示了完整的网格地面,黄色玩家球位于左上角,红色目标球位于中右区域,网格线清晰可见,提供了良好的空间定位参考。

透视角度展示了 3D 效果,可以看到地面网格的透视变化和球体的立体感,底部的 AR 控制图标清晰可见。

控制台输出显示了玩家从 [3,17] 移动到 [9,9] 的过程,最终成功与目标重合,得分为 1。

得分后,目标切换,黄色球和红色球的位置发生了变化,得分后目标球自动移动到了新的随机位置 [0,15]。

控制台日志详细记录了目标创建过程,包括网格坐标、世界坐标、Mesh 名称和场景中的对象总数,便于调试。

卡点回顾
卡点1:JSAR 输入事件不触发
问题:初期使用 Babylon.js 的 ActionManager,点击事件完全无响应。
解决:
-
发现 JSAR 需要显式调用
watchInputEvent() -
改用
spatialDocument.addEventListener('mouse', ...)模式
卡点2:鼠标坐标映射不准确
问题:
-
玩家位置始终为 [0,0]
-
坐标范围在不同设备上不一致
-
出现 (0,0) 无效坐标污染数据
解决方案:
// 1. 过滤无效坐标
if (mouseX === 0 || mouseY === 0) return;
// 2. 动态范围追踪
minMouseX = Math.min(minMouseX, mouseX);
maxMouseX = Math.max(maxMouseX, mouseX);
// 3. 归一化映射
const normalizedX = (mouseX - minMouseX) / rangeX;
playerGridX = Math.floor(normalizedX * GRID_SIZE);
项目结构
jsar-rokid/
├── main.xsml # XSML 场景入口
├── lib/
│ └── main.ts # 游戏主逻辑(255 行)
├── package.json # 项目配置
├── model/
│ └── welcome.glb # 3D 模型资源
└── icon.png # 应用图标
开发总结
本项目成功实现了 JSAR 框架下的鼠标交互机制,通过动态坐标映射算法解决了跨设备输入适配问题。简洁的游戏设计验证了 JSAR 在 AR 应用开发中的可行性,为后续更复杂的空间交互应用奠定了基础。
关键技术突破在于理解 JSAR 的输入事件模型,以及实现自适应的坐标归一化算法。这些经验对于开发 Rokid AR 应用具有重要的参考价值。
评论(0)