技术栈简介
在决定开发这个 AR 小游戏时,选择了以下技术栈:
- Unity 2022.3 LTS (Long Term Support):作为 Unity 的长期支持版本,它在性能优化和第三方库兼容性上达到了一个极佳的平衡点。对于 AR 开发这种对底层稳定性要求极高的场景,LTS 版本能有效避免很多编辑器 Bug。
- Rokid UXR 3.0 SDK:这是 Rokid 官方提供的核心开发套件。它集成了 AR 相机配置、空间定位(6DoF/3DoF)、手势交互模组以及完善的射线检测系统。选择 3.0 版本,是因为它在资源占用和 API 的易用性上做了大量减法,非常适合快速原型验证。
为什么选择这套组合? 核心原因只有两个字:效率。消消乐这种游戏逻辑非常成熟,不希望把精力浪费在配置 Android 环境或调试底层传感器数据上。Unity 2022 的成熟生态,加上 UXR 3.0 这种开箱即用的 SDK,能让我们在短时间内完成从构思到真机运行的全过程。这种快速闭环能力,对于产品团队验证新交互形态至关重要。
思路迁移
在传统的手机屏幕上,消消乐是纯粹的 2D 体验。但在 AR 场景下,需要进行重构。
结构
- 棋盘(Board):在手机上是背景图,在 AR 里则是悬浮在空间中的逻辑面板。它必须有明确的 3D 坐标(Z 轴),以确保用户不会感到视觉压迫。
- 宝石(Gem):不再是单纯的 UI 图片,而是拥有 Collider(碰撞体)的游戏对象。它们必须能响应空间中的射线触发,而不仅仅是屏幕点击。
- UI 系统:Score 和 Moves 如果放在屏幕四个角,会导致用户眼睛频繁调焦,产生疲劳。在 AR 中,将 UI 固定在棋盘上方的左右两侧,随棋盘一起锚定在空间中。
交互映射
手机上的操作是手指在屏幕上拖拽,而 Rokid AR 眼镜支持手持终端射线或手势追踪。开发思路是将玩家的射线输入(或模拟鼠标输入)映射到 3D 空间的物理射线检测(Raycast)上。当射线击中宝石并产生滑动位移时,逻辑层计算出滑动的向量方向(上下左右),从而触发宝石交换。
核心实现思路
在实现层面,采用了高度解耦的设计,将场景初始化、游戏管理、棋盘逻辑、单体交互和预制体生成完全分开。这种做法的好处是,即便未来要把宝石从圆球换成 3D 恐龙,逻辑代码也几乎不需要修改。
场景搭建与一键初始化 (GameSetup)
在 AR 项目里,往往不想手动在 Hierarchy 面板里拖入几十个宝石。因此,编写了 GameSetup.cs。它就像是游戏的发令枪,在运行瞬间自动创建相机、Canvas、文本,并根据代码生成的预制体初始化棋盘。
关键代码:GameSetup.cs
using UnityEngine;
using UnityEngine.UI;
public class GameSetup : MonoBehaviour {
void Start() {
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();
}
}

