体验开场
戴上 Rokid AR Lite,原本平淡的办公桌面瞬间变得有趣。设备启动后,一块晶莹剔透的 8×8 宝石棋盘凭空浮现,稳稳锚定在真实空间里。转动头部时,能清晰感受到棋盘的厚度感。
界面左上角实时跳动 Score,右上角显示剩余 Moves。伸出手利用射线交互滑动两颗宝石,清脆音效伴随粒子消散,三颗同色宝石瞬间消除,上方宝石顺势滑落填补空缺。这不是科幻电影,而是基于 Unity 2022 LTS 与 Rokid UXR 3.0 SDK 开发的轻量级 AR 尝试。将经典玩法带入空间计算时代,背后的'空间映射'思路和开发细节值得每一位 AR 开发者思考。
技术栈简介
决定开发这个 AR 小游戏时,我们选定了这套'黄金组合':
- Unity 2022.3 LTS:长期支持版本,性能优化和第三方库兼容性极佳。AR 开发对底层稳定性要求高,LTS 版能有效避免编辑器 Bug。
- Rokid UXR 3.0 SDK:官方核心套件,集成 AR 相机、6DoF/3DoF 定位、手势交互及射线检测。3.0 版本在资源占用和 API 易用性上做了大量减法,适合快速原型验证。
核心原因只有两个字:效率。消消乐逻辑成熟,我们不希望把精力浪费在配置 Android 环境或调试传感器数据上。Unity 2022 的成熟生态加上 UXR 3.0 的'开箱即用',能让我们在短时间内完成从构思到真机运行。这种快速闭环能力,对产品团队验证新交互形态至关重要。
思路迁移
传统手机屏幕是纯粹的 2D 体验,AR 场景下则需进行'升维重构'。
结构差异
- 棋盘(Board):不再是背景图,而是悬浮在空间中的'逻辑面板'。必须有明确的 3D 坐标(Z 轴),确保用户无视觉压迫感。
- 宝石(Gem):拥有 Collider 的游戏对象,响应空间射线触发,而非单纯屏幕点击。
- UI 系统:避免放在屏幕四角导致眼睛频繁调焦。我们将 UI 固定在棋盘上方左右两侧,随棋盘一起锚定在空间中。
交互映射
手机操作是手指拖拽,Rokid AR 眼镜支持手持终端射线或手势追踪。我们将玩家的射线输入映射到 3D 空间的物理射线检测(Raycast)上。当射线击中宝石并产生滑动位移时,逻辑层计算向量方向(上下左右),触发交换。
核心实现思路
实现层面采用高度解耦设计,将场景初始化、游戏管理、棋盘逻辑、单体交互和预制体生成完全分开。好处是即便未来要把宝石从'圆球'换成'3D 恐龙',逻辑代码也几乎无需修改。
场景搭建与一键初始化 (GameSetup)
AR 项目中,手动在 Hierarchy 面板拖入几十个宝石太繁琐。我们编写了 GameSetup.cs,作为游戏的'发令枪',运行瞬间自动创建相机、Canvas、文本,并根据代码生成的预制体初始化棋盘。
关键代码:GameSetup.cs
using UnityEngine;
using UnityEngine.UI;
public class GameSetup : MonoBehaviour {
void () {
SetupGame();
}
{
Camera mainCamera = Camera.main;
(mainCamera != ) {
mainCamera.transform.position = Vector3(, , );
mainCamera.orthographic = ;
mainCamera.orthographicSize = ;
mainCamera.backgroundColor = Color(, , );
}
GameObject canvasObj = GameObject();
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObj.AddComponent<CanvasScaler>();
canvasObj.AddComponent<GraphicRaycaster>();
GameObject scoreObj = GameObject();
scoreObj.transform.SetParent(canvasObj.transform);
Text scoreText = scoreObj.AddComponent<Text>();
scoreText.text = ;
scoreText.fontSize = ;
scoreText.color = Color.white;
scoreText.alignment = TextAnchor.UpperLeft;
scoreText.font = Resources.GetBuiltinResource<Font>();
RectTransform scoreRect = scoreObj.GetComponent<RectTransform>();
scoreRect.anchorMin = Vector2(, );
scoreRect.anchorMax = Vector2(, );
scoreRect.pivot = Vector2(, );
scoreRect.anchoredPosition = Vector2(, );
scoreRect.sizeDelta = Vector2(, );
GameObject movesObj = GameObject();
movesObj.transform.SetParent(canvasObj.transform);
Text movesText = movesObj.AddComponent<Text>();
movesText.text = ;
movesText.fontSize = ;
movesText.color = Color.white;
movesText.alignment = TextAnchor.UpperRight;
movesText.font = Resources.GetBuiltinResource<Font>();
RectTransform movesRect = movesObj.GetComponent<RectTransform>();
movesRect.anchorMin = Vector2(, );
movesRect.anchorMax = Vector2(, );
movesRect.pivot = Vector2(, );
movesRect.anchoredPosition = Vector2(, );
movesRect.sizeDelta = Vector2(, );
GameObject gmObj = GameObject();
GameManager gm = gmObj.AddComponent<GameManager>();
gm.scoreText = scoreText;
gm.movesText = movesText;
GameObject[] gemPrefabs = GameObject[];
Color[] colors = Color[] {
Color(, , ),
Color(, , ),
Color(, , ),
Color(, , ),
Color(, , ),
Color(, , )
};
[] names = [] { , , , , , };
( i = ; i < ; i++) {
gemPrefabs[i] = GemPrefabCreator.CreateGemPrefab(colors[i], names[i]);
gemPrefabs[i].SetActive();
}
GameObject boardObj = GameObject();
Board board = boardObj.AddComponent<Board>();
board.width = ;
board.height = ;
board.gemSpacing = ;
board.gemPrefabs = gemPrefabs;
Debug.Log();
}
}

