Rokid UXR 的手势追踪空间贪吃蛇小游戏实战开发【含 工程源码 和 最终完成APK】
一、项目介绍
本文将带你基于 Rokid UXR SDK 3.0.3 + 团结引擎
开发一个沉浸式的 AR 空间小游戏——贪吃蛇。
在这个游戏中:
- 玩家只需移动 食指指尖,蛇头就会在空间中跟随移动;
- 吃到食物后,蛇会增长一节身体;
- 游戏内带有倒计时机制,限定时间内比拼得分。
这是一个非常适合初学者上手的 Rokid AR 实战案例。
如下为真机演示视频(学完该教程即可完成)
二、环境准备
- 设备:Rokid AR Studio眼镜
- SDK:Rokid UXR SDK 3.0.3
- 引擎:团结引擎 1.6.5
注:全文必须在官方SDK Rokid UXR SDK 3.0.3 已完成 接入指南 后,也就是完成如下章节后才可开始学习(custom.rokid.com/prod/rokid_web/c88be4bcde4c42c0b8b53409e1fa1701/pc/cn/8ad2e74dbd7c4f5bb21a964dbdd5512d.html?documentId=965ccb56e0284976ab0c701346f5833e)
三、核心功能拆解
1. 蛇头跟随食指指尖
在 SnakeGame 脚本中,我们通过 GesEventInput.Instance.GetSkeletonPose 获取 左右手食指指尖的 3D 坐标,让蛇头跟随指尖进行移动。
优先使用右手,其次左手,保证游戏一直有控制点。:
Pose rightPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.RightHand);
Pose leftPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.LeftHand);
通过 Lerp 平滑移动 + Slerp 平滑旋转,实现蛇头跟随:
- 保证动作不生硬
- 可以自由在空间里画出路径
2. 蛇身增长
蛇头吃到食物时也就是OnTriggerEnter,会调用 AddBodyPart方法:
- 将食物对象转化为蛇身
- 移除食物物理组件,替换成蛇身材质
- 放入
bodyParts队列,自动跟随蛇头轨迹
这里可以加入音效 eatSound,提升游玩沉浸感。
3. 食物生成
由 FoodManager 控制:
- 在一个 区域标定物体(areaMarker) 内随机生成食物
- 可随机指定材质(红/蓝/绿等),提升趣味性
- 每次被吃掉后重新生成
4. 倒计时机制
由 CountdownTimer 脚本实现:
- UI 显示倒计时(使用TextMeshPro 或 Unity Text)
- 倒计时结束时,显示「游戏结束」面板
四、准备美术素材和音效
1、蛇头与蛇身 这里我使用基础 纯方块 Cube 搭建,方便观察和跟随
-
- SnakeHead (空物体, 位置在 0,0,0)
- ├── HeadCube (主体)
- ├── EyeLeft (左眼)
- └── EyeRight (右眼)
每个物体的属性
如下旋转皆为:(0, 0, 0)
1. SnakeHead (空物体)
位置:(0, 0, 0)
缩放:(1, 1, 1) 只是一个父物体容器,下面放真正的方块。
2. HeadCube (主体方块)
父物体:SnakeHead
组件:Cube
位置:(0, 0, 0) (保持原点)
缩放:(1, 1, 1.5) 作为蛇头的长方体。
3. EyeLeft (左眼方块)
父物体:SnakeHead
组件:Cube
位置:(-0.25, 0.25, 0.8) 在蛇头前面靠左上。
缩放:(0.2, 0.2, 0.2)
材质:这里我使用的是蓝色
- 4. EyeRight (右眼方块)
父物体:SnakeHead
组件:Cube
位置:(0.25, 0.25, 0.8) 在蛇头前面靠右上。
缩放:(0.2, 0.2, 0.2)
材质:这里我使用的是橘色

2、蛇身 建立1个 Sphere 预制体,由于需要不需要碰撞,需要不使用重力,需要在属性面板加一个刚体组件,且取消勾选重力,并勾选运动学,我这里制作了一个默认材质Body

2、食物 由于食物是可以转化为身体的,因此只需要复制粘贴蛇身预制体即可,我们这里可以随便新建几个材质,待会放到FoodManager 中,让脚本随机给食物赋材质,其次由于使用OnTriggerEnter,因此需要新建一个tag,并命名为Food,当然如果不改tag,在接下来的FoodManager 中会自动赋予 Food 标签

3、生成区域 需要在场景里放置一个Sphere作为 areaMarker,其缩放(Scale)决定食物生成的范围

四、脚本实现
在完成了场景物体和材质的准备之后,我们开始进入核心代码实现。整个游戏主要由三个脚本构成:
- SnakeGame.cs —— 控制蛇头移动、蛇身跟随和增长逻辑
- FoodManager.cs —— 管理食物的生成与刷新
- CountdownTimer.cs —— 游戏倒计时与结束逻辑
另外,还有一个小脚本 SnakeHeadCollision 专门用于检测蛇头吃到食物的碰撞,为了省略我和SnakeGame写在了一起。
1. SnakeGame —— 蛇的核心控制
这是整个游戏的主脚本,功能包括:
- 获取手势食指指尖位置
- 控制蛇头跟随手指移动
- 记录蛇头的运动轨迹
- 控制蛇身跟随轨迹
- 吃到食物后新增蛇身
核心逻辑分为几个部分:
(1)获取食指位置
通过 Rokid UXR 的手势接口,实时获取左右手食指尖的位置,优先右手:
Pose rightPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.RightHand);
Pose leftPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.LeftHand);
(2)蛇头移动与旋转
- 使用
Vector3.Lerp让蛇头平滑跟随手指位置; - 使用
Quaternion.Slerp让蛇头始终朝向前进方向:
snakeHead.position = Vector3.Lerp(snakeHead.position, targetPosition, moveSpeed * Time.deltaTime);
Quaternion targetRot = Quaternion.LookRotation(moveDirection, Vector3.up);
snakeHead.rotation = Quaternion.Slerp(snakeHead.rotation, targetRot, rotateSpeed * Time.deltaTime);
(3)蛇身跟随
蛇的身体之所以能一节一节地跟随蛇头移动,靠的是一个 轨迹记录 + 队列追踪 的思路:
-
- 轨迹记录 每一帧,都会把蛇头的位置记录到
positionsHistory列表里; - 这样就相当于在空间里留下了一条「移动痕迹」。
- 轨迹记录 每一帧,都会把蛇头的位置记录到
// 记录蛇头轨迹
positionsHistory.Add(snakeHead.position);
-
- 逐节取点跟随 蛇身的每一节,都要沿着蛇头走过的轨迹,保持一定间隔;
- 用
(i+1) * bodySpacing来计算第 i 节身体应该落在哪个轨迹点上; - 然后从
positionsHistory里取出对应点,设置为蛇身的位置。
for (int i = 0; i < bodyParts.Count; i++)
{
float spacingOffset = (i + 1) * bodySpacing;
int index = positionsHistory.Count - 1 - (int)(spacingOffset / 0.1f);
index = Mathf.Clamp(index, 0, positionsHistory.Count - 1);
Vector3 point = positionsHistory[index];
bodyParts[i].position = point;
}
-
- 旋转对齐 为了让蛇身自然朝向前进方向,需要根据轨迹点的方向来调整旋转:
Vector3 dir = positionsHistory[Mathf.Max(index - 1, 0)] - point;
if (dir.sqrMagnitude > 0.001f)
bodyParts[i].rotation = Quaternion.LookRotation(dir, Vector3.up);
-
- 限制历史长度 如果不限制
positionsHistory的大小,时间长了会无限增长,占用内存; - 所以加一个上限(例如 3000),超出时删除最旧的点。
- 限制历史长度 如果不限制
if (positionsHistory.Count > 3000)
positionsHistory.RemoveAt(0);
📌 最终效果:
蛇头移动时,蛇身就像沿着轨迹排队跟随,前进流畅,不会乱掉,也不会「穿模」。
(4)新增蛇身
当蛇头和食物发生碰撞时,会调用 AddBodyPart(),让食物「变身」为蛇身的一节。
-
- 移除物理组件 食物原本有
Rigidbody和Collider,用于被检测; - 变成蛇身后,就不需要再参与物理运算,否则可能继续触发碰撞;
- 所以要移除这些组件:
- 移除物理组件 食物原本有
Rigidbody rb = newPart.GetComponent<Rigidbody>();
if (rb != null) Destroy(rb);
Collider col = newPart.GetComponent<Collider>();
if (col != null) Destroy(col);
-
- 替换材质 为了区分「蛇身」和「食物」,这里通过 Inspector 拖入一个
defaultBodyMaterial; - 吃掉后,自动把食物材质换成统一的蛇身材质。
- 替换材质 为了区分「蛇身」和「食物」,这里通过 Inspector 拖入一个
Renderer rend = newPart.GetComponent<Renderer>();
if (rend != null && defaultBodyMaterial != null)
rend.material = defaultBodyMaterial;
-
- 加入蛇身队列 把新增加的一节放进
bodyParts列表; - 这样它在下一帧
Update()时,就会自动开始跟随轨迹。
- 加入蛇身队列 把新增加的一节放进
newPart.transform.SetParent(this.transform);
bodyParts.Add(newPart.transform);
-
- 更新得分 UI 每次蛇身长度增加,更新
foodEatenCount; - UI 上显示最新得分,让玩家直观感受到成长。
- 更新得分 UI 每次蛇身长度增加,更新
foodEatenCount = bodyParts.Count - 1;
if (foodEatenCountText != null)
foodEatenCountText.text = "得分: " + foodEatenCount;
-
- 播放音效反馈 如果配置了
eatSound,每次增长时播放,给玩家听觉反馈。
- 播放音效反馈 如果配置了
if (eatSound != null && audioSource != null)
audioSource.PlayOneShot(eatSound);
2. SnakeHeadCollision —— 碰撞检测
蛇头物体上挂载了这个脚本,用于检测 OnTriggerEnter:
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag(foodTag))
{
if (other.gameObject.GetComponent<AlreadyEaten>() == null)
{
other.gameObject.AddComponent<AlreadyEaten>();
snakeGame.AddBodyPart(other.gameObject);
}
}
}
这里通过添加一个 AlreadyEaten 标记,避免同一个食物被重复触发。可以直接新建一个AlreadyEaten 脚本,不需要写内容

完整 SnakeGame.cs + SnakeHeadCollision.cs脚本
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using Rokid.UXR.Interaction;
public class SnakeGame : MonoBehaviour
{
[Header("拖拽设置")]
public GameObject snakeHeadPrefab;
public List<GameObject> snakeBodyPrefabs;
public Transform rightIndexTipTransform;
public Transform leftIndexTipTransform;
[Header("移动参数")]
public float moveSpeed = 5f;
public float rotateSpeed = 10f;
public float bodySpacing = 0.5f;
[Header("吃东西设置")]
public string foodTag = "Food";
[Header("游戏统计")]
public int foodEatenCount = 0;
[Header("蛇身材质设置")]
public Material defaultBodyMaterial;
[Header("UI 显示")]
public TextMeshProUGUI foodEatenCountText;
public AudioClip eatSound;
private AudioSource audioSource;
private Transform snakeHead;
public List<Transform> bodyParts = new List<Transform>();
private List<Vector3> positionsHistory = new List<Vector3>();
void Start()
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
Vector3 startPosition = Vector3.zero;
if (rightIndexTipTransform != null && rightIndexTipTransform.gameObject.activeInHierarchy)
startPosition = rightIndexTipTransform.position;
else if (leftIndexTipTransform != null && leftIndexTipTransform.gameObject.activeInHierarchy)
startPosition = leftIndexTipTransform.position;
if (snakeHeadPrefab != null)
{
GameObject headInstance = Instantiate(snakeHeadPrefab, startPosition, Quaternion.identity);
snakeHead = headInstance.transform;
Rigidbody rb = headInstance.GetComponent<Rigidbody>();
if (rb == null) rb = headInstance.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
Collider col = headInstance.GetComponent<Collider>();
if (col == null) col = headInstance.AddComponent<SphereCollider>();
col.isTrigger = true;
SnakeHeadCollision collision = headInstance.AddComponent<SnakeHeadCollision>();
collision.Init(this, foodTag);
snakeHead.transform.SetParent(this.transform);
}
foreach (GameObject bodyPrefab in snakeBodyPrefabs)
{
if (bodyPrefab != null)
{
GameObject body = Instantiate(bodyPrefab, startPosition, Quaternion.identity);
body.transform.SetParent(this.transform);
bodyParts.Add(body.transform);
}
}
}
void Update()
{
if (snakeHead == null) return;
Pose rightPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.RightHand);
Pose leftPose = GesEventInput.Instance.GetSkeletonPose(SkeletonIndexFlag.INDEX_FINGER_TIP, HandType.LeftHand);
Vector3 targetPosition = Vector3.zero;
bool hasHand = false;
if (rightPose.position != Vector3.zero)
{
targetPosition = rightPose.position;
hasHand = true;
}
else if (leftPose.position != Vector3.zero)
{
targetPosition = leftPose.position;
hasHand = true;
}
if (!hasHand) return;
snakeHead.position = Vector3.Lerp(snakeHead.position, targetPosition, moveSpeed * Time.deltaTime);
Vector3 moveDirection = (targetPosition - snakeHead.position).normalized;
if (moveDirection.sqrMagnitude > 0.001f)
{
Quaternion targetRot = Quaternion.LookRotation(moveDirection, Vector3.up);
snakeHead.rotation = Quaternion.Slerp(snakeHead.rotation, targetRot, rotateSpeed * Time.deltaTime);
}
positionsHistory.Add(snakeHead.position);
for (int i = 0; i < bodyParts.Count; i++)
{
float spacingOffset = (i + 1) * bodySpacing;
int index = positionsHistory.Count - 1 - (int)(spacingOffset / 0.1f);
index = Mathf.Clamp(index, 0, positionsHistory.Count - 1);
Vector3 point = positionsHistory[index];
bodyParts[i].position = point;
Vector3 dir = positionsHistory[Mathf.Max(index - 1, 0)] - point;
if (dir.sqrMagnitude > 0.001f)
bodyParts[i].rotation = Quaternion.LookRotation(dir, Vector3.up);
}
if (positionsHistory.Count > 3000)
positionsHistory.RemoveAt(0);
}
public void AddBodyPart(GameObject newPart)
{
int beforeCount = bodyParts.Count;
FoodManager fm = FindFirstObjectByType<FoodManager>();
if (fm != null) fm.NotifyFoodEaten();
Rigidbody rb = newPart.GetComponent<Rigidbody>();
if (rb != null) Destroy(rb);
Collider col = newPart.GetComponent<Collider>();
if (col != null) Destroy(col);
Renderer rend = newPart.GetComponent<Renderer>();
if (rend != null && defaultBodyMaterial != null)
rend.material = defaultBodyMaterial;
newPart.transform.SetParent(this.transform);
bodyParts.Add(newPart.transform);
foodEatenCount = bodyParts.Count - 1;
if (bodyParts.Count > beforeCount && eatSound != null && audioSource != null)
{
audioSource.PlayOneShot(eatSound);
if (foodEatenCountText != null)
foodEatenCountText.text = "得分: " + foodEatenCount;
}
}
}
public class SnakeHeadCollision : MonoBehaviour
{
private SnakeGame snakeGame;
private string foodTag;
public void Init(SnakeGame game, string tag)
{
snakeGame = game;
foodTag = tag;
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag(foodTag))
{
if (other.gameObject.GetComponent<AlreadyEaten>() == null)
{
other.gameObject.AddComponent<AlreadyEaten>();
snakeGame.AddBodyPart(other.gameObject);
}
}
}
}
3. FoodManager —— 食物生成
在贪吃蛇的玩法中,食物是让蛇不断成长的关键元素。本项目里,FoodManager 负责整个食物的 生成、刷新、随机外观。
(1)区域定义
我们在场景里放置一个 areaMarker(比如一个 Cube 或 Sphere),它的 位置(position) 和 缩放(scale) 定义了整个食物生成的区域:
areaMarker.position= 生成区域的中心点areaMarker.localScale= 区域的宽度、高度、深度
这样可以很直观地在场景中调整「食物能出现的范围」。
spawnAreaCenter = areaMarker.position;
spawnAreaSize = areaMarker.localScale;
(2)随机生成位置
在区域范围内,随机一个点作为食物生成位置:
Vector3 randomPos = spawnAreaCenter + new Vector3(
Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
Random.Range(-spawnAreaSize.y / 2, spawnAreaSize.y / 2),
Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
);
这里的 Random.Range 保证:
- X/Y/Z 坐标都在
[-size/2, +size/2]的范围内; - 这样食物的分布是均匀的;
- 且永远不会超出
areaMarker定义的区域。
📌 效果:食物可能出现在区域的任意地方,增加随机性和挑战性。
(3)多预制体支持
为了让食物的外观更丰富,foodPrefabs 数组里可以放多个不同形状的预制体(比如球体、方块、胶囊体等)。生成时随机挑选一个:
GameObject prefab = foodPrefabs[Random.Range(0, foodPrefabs.Length)];
currentFood = Instantiate(prefab, randomPos, Quaternion.identity);
(4)随机材质支持
我们还可以在 Inspector 中拖入多个 randomMaterials 材质,比如红、绿、蓝、黄,生成时随机赋值:
if (randomMaterials != null && randomMaterials.Length > 0)
{
Renderer rend = currentFood.GetComponent<Renderer>();
if (rend != null)
{
Material mat = randomMaterials[Random.Range(0, randomMaterials.Length)];
rend.material = mat;
}
}
这样同样的形状,也能因为材质的不同而显得更有变化。
(5)刷新逻辑
食物一旦被蛇吃掉(变成蛇身),SnakeGame 会调用 FoodManager.NotifyFoodEaten(),把 currentFood 置为 null。
下一帧 Update() 时,检测到没有食物,就会自动重新生成一个新的。
public void NotifyFoodEaten()
{
currentFood = null; // 标记没有食物,下帧会生成新的
}
📌 效果:整个游戏过程永远不会出现「没有食物」的情况,保证玩法持续。
(6)辅助可视化
为了让开发时更直观,OnDrawGizmosSelected() 会在 Scene 视图里绘制一个绿色的方框,显示生成范围。
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
if (areaMarker != null)
{
Gizmos.DrawWireCube(areaMarker.position, areaMarker.localScale);
}
}
完整 FoodManager.cs 脚本
using UnityEngine;
public class FoodManager : MonoBehaviour
{
[Header("生成配置")]
public GameObject[] foodPrefabs; // 可生成的食物预制体数组(Cube/Sphere/Capsule等)
public Transform areaMarker; // 区域标定物体,决定食物生成范围
[Header("材质设置")]
public Material[] randomMaterials; // 随机材质数组(红/绿/蓝等)
private GameObject currentFood; // 当前存在的食物实例
private Vector3 spawnAreaCenter; // 区域中心
private Vector3 spawnAreaSize; // 区域大小
void Update()
{
// 根据标定物体更新区域
if (areaMarker != null)
{
spawnAreaCenter = areaMarker.position;
spawnAreaSize = areaMarker.localScale;
}
// 如果当前没有食物,就生成一个新的
if (currentFood == null)
{
SpawnFood();
}
}
/// <summary>
/// 生成一个新的食物
/// </summary>
private void SpawnFood()
{
if (foodPrefabs.Length == 0) return;
// 随机生成位置(在 areaMarker 范围内)
Vector3 randomPos = spawnAreaCenter + new Vector3(
Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
Random.Range(-spawnAreaSize.y / 2, spawnAreaSize.y / 2),
Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
);
// 随机选择一个预制体
GameObject prefab = foodPrefabs[Random.Range(0, foodPrefabs.Length)];
currentFood = Instantiate(prefab, randomPos, Quaternion.identity);
// 给食物设置 Food 标签(用于碰撞检测)
currentFood.tag = "Food";
// 随机材质
if (randomMaterials != null && randomMaterials.Length > 0)
{
Renderer rend = currentFood.GetComponent<Renderer>();
if (rend != null)
{
Material mat = randomMaterials[Random.Range(0, randomMaterials.Length)];
rend.material = mat;
}
}
// 设为子物体,方便管理
currentFood.transform.SetParent(this.transform);
}
/// <summary>
/// 被 SnakeGame 调用:当食物被吃掉并变成蛇身时
/// </summary>
public void NotifyFoodEaten()
{
currentFood = null; // 标记当前没有食物,下帧会重新生成
}
/// <summary>
/// 在 Scene 视图绘制区域辅助框
/// </summary>
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
if (areaMarker != null)
{
Gizmos.DrawWireCube(areaMarker.position, areaMarker.localScale);
}
}
}
4. CountdownTimer —— 倒计时
在贪吃蛇游戏里,如果没有时间限制,玩家可能一直玩下去。
为了增加紧迫感和挑战性,我们加入了 倒计时机制,用 CountdownTimer 脚本来管理。
(1)功能需求
-
- 控制总时长 在 Inspector 面板输入「分钟 + 秒」;
- 运行时自动转化为总秒数,开始倒计时。
-
- 每帧递减 在
Update()里,每帧减去Time.deltaTime; - 这样倒计时和实际时间保持一致,不会受帧率影响。
- 每帧递减 在
-
- 更新 UI 剩余时间会显示在
TextMeshProUGUI或 Unity 自带Text上; - 格式为
MM:SS,例如 01:29。
- 更新 UI 剩余时间会显示在
时间结束事件
当 timeRemaining <= 0,触发 OnTimerEnd():
-
-
- 隐藏游戏 UI(例如操作面板、计分板);
- 显示「Game Over」界面,让玩家看到结果。
-
(2)倒计时逻辑
核心逻辑如下:
if (isRunning && timeRemaining > 0)
{
timeRemaining -= Time.deltaTime; // 每帧递减
UpdateUI(); // 更新 UI
}
else if (isRunning && timeRemaining <= 0)
{
isRunning = false; // 停止倒计时
OnTimerEnd(); // 触发结束逻辑
}
timeRemaining存储剩余时间(秒);isRunning表示倒计时是否正在运行;- 时间减到 0 会自动调用
OnTimerEnd()。
(3)UI 更新
UpdateUI() 把秒数换算成分钟和秒,并显示在 UI 上:
int displayMinutes = Mathf.FloorToInt(timeRemaining / 60);
int displaySeconds = Mathf.FloorToInt(timeRemaining % 60);
string timeStr = string.Format("{0:00}:{1:00}", displayMinutes, displaySeconds);
if (timerText != null)
timerText.text = timeStr;
if (uiText != null)
uiText.text = timeStr;
这样保证无论用 TextMeshPro 还是 Unity 自带 UI,都能正确显示。
(4)结束时触发
OnTimerEnd() 用来切换游戏 UI:
private void OnTimerEnd()
{
Debug.Log("倒计时结束!");
// 隐藏游戏中的 UI(比如分数面板、操作按钮)
foreach (var obj in objectsToHide)
{
if (obj != null) obj.SetActive(false);
}
// 显示结束界面(比如 Game Over 面板)
foreach (var obj in objectsToShow)
{
if (obj != null) obj.SetActive(true);
}
}
这里的 objectsToHide 和 objectsToShow 可以在 Inspector 面板里自由拖拽,灵活控制结束后的界面切换。
(5)重新开始
脚本里还提供了 RestartTimer() 方法,可以随时重置倒计时,重新开始一局游戏。
public void RestartTimer()
{
timeRemaining = minutes * 60 + seconds; // 重新设置总时长
isRunning = true; // 开始运行
UpdateUI(); // 更新 UI
}
完整 CountdownTimer.cs 脚本
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class CountdownTimer : MonoBehaviour
{
[Header("倒计时设置")]
public int minutes = 1; // 输入分钟
public int seconds = 30; // 输入秒
[Header("UI 显示 (可选)")]
public TextMeshProUGUI timerText; // TextMeshPro 显示
public Text uiText; // Unity 自带 Text 显示
[Header("倒计时结束后的操作")]
public GameObject[] objectsToHide; // 结束后隐藏的物体
public GameObject[] objectsToShow; // 结束后显示的物体
private float timeRemaining; // 剩余时间(秒)
private bool isRunning = false; // 是否在运行
private void Awake()
{
RestartTimer(); // 游戏开始时自动启动
}
void Update()
{
if (isRunning && timeRemaining > 0)
{
timeRemaining -= Time.deltaTime; // 每帧递减
if (timeRemaining < 0) timeRemaining = 0;
UpdateUI();
}
else if (isRunning && timeRemaining <= 0)
{
isRunning = false;
OnTimerEnd(); // 倒计时结束
}
}
// 更新 UI 显示
private void UpdateUI()
{
int displayMinutes = Mathf.FloorToInt(timeRemaining / 60);
int displaySeconds = Mathf.FloorToInt(timeRemaining % 60);
string timeStr = string.Format("{0:00}:{1:00}", displayMinutes, displaySeconds);
if (timerText != null)
timerText.text = timeStr;
if (uiText != null)
uiText.text = timeStr;
}
// 倒计时结束时调用
private void OnTimerEnd()
{
Debug.Log("倒计时结束!");
foreach (var obj in objectsToHide)
{
if (obj != null) obj.SetActive(false);
}
foreach (var obj in objectsToShow)
{
if (obj != null) obj.SetActive(true);
}
}
// 重置并重新开始倒计时
public void RestartTimer()
{
timeRemaining = minutes * 60 + seconds;
isRunning = true;
UpdateUI();
}
}
五、配置各个脚本的参数
在完成了脚本编写和物体搭建后,我们需要在 Unity / 团结引擎的 Inspector 面板中给脚本配置参数,保证游戏能正常运行。以下步骤逐一对应三个主要脚本。
1. 配置 SnakeGame.cs
挂载位置:
- 在场景中新建一个空物体
SnakeGame,把SnakeGame.cs脚本挂载到上面。
Inspector 参数设置:
-
- Snake Head Prefab 拖入制作好的蛇头预制体(SnakeHead)。
- 蛇头内部包含 HeadCube + 眼睛两个小方块。
-
- Snake Body Prefabs 拖入 Sphere 预制体(蛇身小球),可以先放一个作为初始长度,也可以留空。
-
- Right Index Tip Transform / Left Index Tip Transform
- 脚本会直接通过
GesEventInput获取,保持空即可。
-
- Move Speed / Rotate Speed 我这里设置的:Move Speed = 3,Rotate Speed = 10,蛇头不会抖动且响应灵敏。
-
- Body Spacing 决定蛇身之间的间隔,推荐 0.8。
-
- Food Tag 默认写成
"Food",不用改。
- Food Tag 默认写成
-
- Default Body Material 拖入你准备好的蛇身材质(Body 材质),让蛇身统一颜色。
-
- Food Eaten Count Text 拖入一个 TextMeshPro UI,用于实时显示得分。
-
- Eat Sound 拖入一段「吃东西」音效(例如咀嚼声)。

2. 配置 FoodManager.cs
挂载位置:
- 在场景中新建一个空物体
FoodManager,挂载FoodManager.cs。
Inspector 参数设置:
-
- Food Prefabs 拖入你制作的食物预制体(可以直接用蛇身的小球 Sphere),至少放一个;
- 可以拖多个(比如方块、球体、胶囊体),游戏会随机选择。
-
- Area Marker 拖入一个场景里的立方体 / 球体,作为生成区域标记;
- 注意:
Position表示中心点,Scale表示生成范围大小。
-
- Random Materials 拖入几种颜色材质(红、蓝、绿、黄),增加食物外观的随机性。
在 Scene 视图选中 FoodManager 时,会看到绿色的框,表示生成范围是否正确。

3. 配置 CountdownTimer.cs
挂载位置:
- 在场景中新建一个 UI 管理器
CountdownTimer,挂载CountdownTimer.cs。
Inspector 参数设置:
-
- Minutes / Seconds 设置倒计时长度,我这里设置的是 1 分钟。
-
- Timer Text / UI Text 如果使用 TextMeshPro,拖入一个
TextMeshProUGUI组件; - 如果使用 Unity 自带 Text,拖入对应组件;
- 两个字段至少填一个即可。
- Timer Text / UI Text 如果使用 TextMeshPro,拖入一个
-
- Objects To Hide 拖入游戏中的与游戏相关的物体和脚本;
- 倒计时结束后,这些物体会被自动隐藏。
-
- Objects To Show 拖入主菜单按钮和得分文本;
- 时间结束后会自动显示。
我这里的显示逻辑是,进入该场景直接开始游戏,在游戏结束后,隐藏所有与游戏相关的脚本物体,显示主菜单和得分UI,点击主菜单Home按钮可切换回开始游戏界面,把该方法绑定到按钮上即可
SceneSwitcher.cs
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneSwitcher : MonoBehaviour
{
// 在括号中直接填写要跳转的场景名称,例如 "GameScene"
public void SwitchScene()
{
SceneManager.LoadScene("GameScene");
// ↑ 把 "GameScene" 改成你要跳转的场景名
}
}



注:1、摄影机位置需置为默认
2、开启6Dof模式

3、RKInput需设置手势追踪

4、需把场景按顺序拖入生成面板

六、打包与安装
当我们在编辑器里调试完成后,最后一步就是 打包并安装到 Rokid AR Studio 眼镜上进行真机体验。
检查场景设置
- 确保游戏的主场景已经添加到File → Build Settings → Scenes In Build 列表中。
- 这里的主菜单场景名称需要和
SceneManager.LoadScene("xxx")里的名字保持一致。
七、结束语
到这里,我们已经完成了一个 基于 Rokid UXR SDK + 团结引擎 的空间贪吃蛇小游戏。从 手势追踪 获取食指尖位置,到 蛇头跟随、蛇身增长、食物生成,再到 倒计时与游戏结束逻辑,我们一步步把传统玩法带进了 AR 空间。
这个项目虽然简单,但涵盖了 Rokid AR 开发的核心要点:
- 如何使用 UXR 获取骨架关节数据并驱动场景对象;
- 如何结合 团结引擎完成动态物体生成与材质管理;
- 如何构建一个完整的「开始 → 进行 → 结束」游戏流程。
希望通过这篇实战,你不仅能收获一个可玩的 AR 小游戏,更能对 Rokid AR 开发流程 有整体的理解。未来,当手势追踪、空间交互与现实环境更好地结合时,我们将能够把更多传统游戏带到现实世界中,真正实现 游戏与现实的融合。
最终完成安装包
工程源码
RokidSnake.zipRokid UXR 的手势追踪空间贪吃蛇小游戏实战开发【含 工程源码 和 最终完成APK】RokidSnake.zip
更多推荐

所有评论(0)