“马上”有惊喜:在 Rokid 灵珠平台上构建 FPS 级 AR 红包雷达应用

2 阅读11分钟

引言

站在 2026 马年春节的门槛上回看,2025 年确实可以被称为 AI 眼镜的“元年”。在那一年里,我们看到眼镜从单纯的显示器进化成了拥有多模态交互能力的智能终端。而在激烈的“百镜大战”之后,如何让 AR 眼镜在春节这种高频生活场景中真正“好用”且“好玩”,成了摆在我们开发者面前的一道必答题。

春节最核心的互动是抢红包。但在过去,这仅仅是手指在屏幕上的机械点击。今年,借着 Rokid 平台契机,我设计并开发了一款基于 Rokid 灵珠平台(AR Lite/Studio)的“AR 红包雷达”。

我的核心思路是:将 FPS(第一人称射击)游戏的沉浸式操控逻辑引入 AR 空间。 用户不再是旁观者,而是通过物理移动和视角旋转,在现实的客厅、年夜饭桌旁,利用类似雷达的引导系统去搜寻、锁定并捕获那些藏在空间里的虚拟红包。这种“马上行动”的交互方式,不仅契合马年的寓意,更打破了春节期间“大家聚在一起却各自玩手机”的冷漠局面。

一、 视角交互逻辑:将“行走”带入空间

在 AR 应用开发中,最核心的矛盾在于“如何定义用户的位姿”。在 Rokid 这种分体式或一体式 AR 眼镜上,用户的视线就是交互的圆心。

我首先构建了一套 FPS 风格的视角操控系统。这套系统在开发阶段能让我们以第一人称视角在 Unity 环境中进行高频迭代,而在最终落地到 Rokid 灵珠平台时,它能够完美平滑地衔接眼镜的 IMU 陀螺仪数据。通过 WASD 键位的平移逻辑和鼠标右键的视角旋转,我模拟了用户在房间内走动寻宝的真实体感。这种设计确保了应用在不同光照、不同复杂的家庭环境下,依然能保持逻辑的一致性。

核心代码 1:CameraAutoMover.cs

using UnityEngine;

/// <summary>/// 空间视角交互类:通过 FPS 逻辑驱动摄像机,实现沉浸式寻宝移动/// </summary>public class CameraAutoMover : MonoBehaviour
{
    [Header("操控设置")]
    public bool mouseControl = true;
    public float mouseSensitivity = 2f;
    public float moveSpeed = 5f;
    public float smoothTime = 0.1f;

    private Vector2 currentRotation;
    private Vector2 targetRotation;
    private Vector3 moveDirection;
    private Vector3 currentVelocity;

    void Start(){
        // 获取初始朝向,确保视野与现实世界对齐
        Vector3 euler = transform.eulerAngles;
        currentRotation = new Vector2(euler.x, euler.y);
        targetRotation = currentRotation;
    }

    void Update(){
        // 允许通过按键切换控制权,提升交互灵活性if (Input.GetKeyDown(KeyCode.Space))
            mouseControl = !mouseControl;

        if (!mouseControl) return;

        HandleMouseLook();
        HandleMovement();
    }

    void HandleMouseLook(){
        if (Input.GetMouseButton(1))
        {
            float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
            float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;

            targetRotation.y += mouseX;
            targetRotation.x -= mouseY;
            // 限制仰角,防止在 AR 环境中产生晕动症
            targetRotation.x = Mathf.Clamp(targetRotation.x, -80f, 80f);
        }

        // 使用平滑插值模拟头部转动的惯性
        currentRotation = Vector2.Lerp(currentRotation, targetRotation, smoothTime * 10f * Time.deltaTime);
        transform.rotation = Quaternion.Euler(currentRotation.x, currentRotation.y, 0);
    }

    void HandleMovement(){
        float horizontal = 0f;
        float vertical = 0f;

        // 模拟用户在物理空间中的位置移动if (Input.GetKey(KeyCode.W)) vertical = 1f;
        if (Input.GetKey(KeyCode.S)) vertical = -1f;
        if (Input.GetKey(KeyCode.A)) horizontal = -1f;
        if (Input.GetKey(KeyCode.D)) horizontal = 1f;

        float height = 0f;
        if (Input.GetKey(KeyCode.E)) height = 1f;
        if (Input.GetKey(KeyCode.Q)) height = -1f;

        Vector3 forward = transform.forward;
        Vector3 right = transform.right;

        moveDirection = forward * vertical + right * horizontal + Vector3.up * height;
        moveDirection.Normalize();

        transform.position += moveDirection * moveSpeed * Time.deltaTime;
    }

    void OnGUI(){
        if (!mouseControl) return;
        GUI.Label(new Rect(10, Screen.height - 60, 400, 40), 
            "视角控制已激活:右键视角旋转 | WASD 空间位移 | 空格键解锁");
    }
}

在实现这段逻辑时,我特别关注了 Vector2.Lerp 的平滑处理。AR 应用中最忌讳的是视角的“瞬移”,这会瞬间摧毁用户的空间感。通过加入平滑因子,即便在 PC 环境下通过键鼠操作,也能模拟出 Rokid 眼镜那种如丝般顺滑的头部追踪体验。


二、 红包目标的“空间智能”:探测与响应

有了交互视角,接下来的问题是:红包如何吸引用户的注意?

在春节这种背景极其复杂的现实场景中(到处是春联、窗花、彩灯),静态的虚拟物体很容易“隐身”。我为红包设计了一套基于距离感知的交互逻辑。当用户通过 Rokid 眼镜观察时,红包会表现出一种“生命力”:

  1. 探测层:当距离较远时,红包呈静默状态,仅在雷达 UI 上显示微弱信号。
  2. 引导层:当进入 5 米范围,红包开始“呼吸”(脉冲缩放)并改变色彩。
  3. 互动层:当用户物理靠近到 1.5 米内,系统会自动触发捕获动效。

这种逻辑不仅提升了趣味性,更重要的一点是:它极大降低了交互的心理门槛。 即使是不擅长使用复杂控制器的长辈,只要戴上眼镜走过去,就能抢到红包。

核心代码 2:RedPacketTarget.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

/// <summary>/// 红包目标逻辑:负责空间距离感知、动态视觉反馈及收集判定/// </summary>public class RedPacketTarget : MonoBehaviour
{
    [Header("红包设置")]
    public string redPacketId;
    public float collectRadius = 1.5f;
    public bool isFound = false;
    public float hintDistance = 5f;

    [Header("视觉反馈")]
    public GameObject hintPanel;
    public Text hintText;
    public float pulseSpeed = 2f;
    public float pulseAmount = 0.2f;

    private Transform playerCamera;
    private Vector3 originalScale;
    private Renderer[] renderers;

    void Start(){
        playerCamera = Camera.main.transform;
        originalScale = transform.localScale;
        renderers = GetComponentsInChildren<Renderer>();

        if (hintPanel != null)
            hintPanel.SetActive(false);
    }

    void Update(){
        if (isFound) return;

        // 实时计算用户与红包的空间物理距离float distance = Vector3.Distance(playerCamera.position, transform.position);

        if (distance < collectRadius)
        {
            CollectRedPacket();
        }
        else if (distance < hintDistance)
        {
            ShowProximityEffect(distance);
        }
        else
        {
            HideProximityEffect();
        }
    }

    void ShowProximityEffect(float distance){
        // 呼吸效果:让红包在复杂的现实背景中更易识别float scale = 1f + Mathf.Sin(Time.time * pulseSpeed) * pulseAmount;
        transform.localScale = originalScale * scale;

        // 根据距离动态改变颜色强度float intensity = 1f - (distance / hintDistance);
        SetHighlight(intensity);

        if (hintPanel != null)
        {
            hintPanel.SetActive(true);
            UpdateHintText(distance);
        }
    }

    void HideProximityEffect(){
        transform.localScale = originalScale;
        SetHighlight(0f);
        if (hintPanel != null && hintPanel.activeSelf)
            hintPanel.SetActive(false);
    }

    void UpdateHintText(float distance){
        if (hintText != null)
        {
            if (distance < 3f) { hintText.text = "🎉 在这儿!"; hintText.color = Color.green; }
            else { hintText.text = "👀 红包信号..."; hintText.color = Color.white; }
        }
    }

    void SetHighlight(float intensity){
        foreach (Renderer r in renderers)
        {
            if (r != null && r.material != null)
            {
                r.material.color = Color.Lerp(Color.red, Color.yellow, intensity);
            }
        }
    }

    public void CollectRedPacket(){
        isFound = true;
        StartCoroutine(CollectEffect());
    }

    IEnumerator CollectEffect(){
        float duration = 0.5f;
        float elapsed = 0f;
        Vector3 startScale = transform.localScale;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / duration;
            transform.localScale = startScale * (1f + t * 2f) * (1f - t);
            yield return null;
        }

        if (ARRedPacketManager.Instance != null)
            ARRedPacketManager.Instance.RegisterFoundRedPacket(this);

        Destroy(gameObject);
    }

    void OnDrawGizmosSelected(){
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, hintDistance);
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position, collectRadius);
    }
}

在红包的视觉表现上,我特意使用了 Mathf.Sin 函数来控制缩放。在 AR 场景中,这种规律的脉冲动画能极大地触发人类视觉系统的边缘检测,即便红包刷在用户的眼角余光处,也能迅速引起警觉。这正是利用 AR 技术解决“信息过载”的一种尝试。


三、 雷达指挥系统:统筹全局的 UI 核心

在春节聚会这种多人、多干扰的动态环境下,用户很容易在寻找过程中迷失方向。为了解决这个问题,我开发了 ARRedPacketManager,它不仅是全局状态的维护者,更是用户在空间中的“战术地图”。

我为 UI 设计了一个不停旋转的雷达扫描线,它的逻辑虽然简单,但在视觉上能够赋予应用一种“专业探测设备”的沉浸感。管理器会实时轮询所有红包的位置,计算出最近的目标,并反馈信号强度。这种设计让寻找红包变成了一个有据可循的“追踪任务”,而非没头苍蝇般的乱撞。

核心代码 3:ARRedPacketManager.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

/// <summary>/// AR 全局管理中心:负责红包雷达扫描、UI 进度同步及游戏逻辑控制/// </summary>public class ARRedPacketManager : MonoBehaviour
{
    public static ARRedPacketManager Instance { get; private set; }

    [Header("UI 控件")]
    public Text statusText;
    public Text distanceText;
    public Text foundCountText;
    public RectTransform scanLine;

    [Header("探测配置")]
    public int totalRedPackets = 5;
    public float detectionRadius = 10f;
    public float radarRotationSpeed = 120f;

    private List<RedPacketTarget> foundRedPackets = new List<RedPacketTarget>();

    void Awake() => Instance = this;

    void Start(){
        UpdateUI();
    }

    void Update(){
        // 视觉动效:雷达扫描线持续旋转if (scanLine != null)
            scanLine.localEulerAngles += new Vector3(0, 0, -radarRotationSpeed * Time.deltaTime);

        UpdateDetection();
    }

    void UpdateDetection(){
        RedPacketTarget[] allPackets = FindObjectsOfType<RedPacketTarget>();
        if (allPackets.Length == 0 && foundRedPackets.Count >= totalRedPackets) return;

        RedPacketTarget nearest = null;
        float nearestDist = float.MaxValue;

        foreach (var packet in allPackets)
        {
            if (packet.isFound) continue;
            float d = Vector3.Distance(Camera.main.transform.position, packet.transform.position);
            if (d < nearestDist)
            {
                nearestDist = d;
                nearest = packet;
            }
        }

        if (nearest != null && nearestDist < detectionRadius)
        {
            statusText.text = "🔍 检测到红包能量信号!";
            statusText.color = Color.yellow;
            distanceText.text = $"目标距离: {nearestDist:F1} 米";
        }
        else
        {
            statusText.text = "👓 移动视角以探测红包...";
            statusText.color = Color.white;
            distanceText.text = "距离: --";
        }
    }

    public void RegisterFoundRedPacket(RedPacketTarget packet){
        if (!foundRedPackets.Contains(packet))
        {
            foundRedPackets.Add(packet);
            UpdateUI();
        }
    }

    void UpdateUI(){
        if (foundCountText != null)
            foundCountText.text = $"已搜集: {foundRedPackets.Count}/{totalRedPackets}";
        
        if (foundRedPackets.Count >= totalRedPackets && totalRedPackets > 0)
        {
            statusText.text = "🎊 恭喜!集齐马年福气! 🎊";
            statusText.color = Color.green;
        }
    }
}

四、 程序化创世:摆脱资源包的负担

在 AR 应用开发中,包体大小是一个隐形杀手。为了让这个“红包雷达”能轻量化部署在 Rokid 灵珠平台上,我采取了 “程序化纹理绘制” 的方案。

GameSetup.cs 脚本中,我并没有使用一张 PNG 图片作为红包贴图。相反,我通过代码在内存中动态创建了一个 128x160 的 Texture2D,并用像素算法画出了一个带金币图案的红包。这种方式不仅让应用在安装瞬间几乎“零占用”,更重要的是,它能让我根据马年春节的主题色,动态调整红包的色值、亮度甚至金边的宽度。

核心代码 4:GameSetup.cs

using UnityEngine;
using UnityEngine.UI;

/// <summary>/// 核心场景构建器:自动生成全局管理器、UI 界面及程序化红包目标/// </summary>public class GameSetup : MonoBehaviour
{
    public int redPacketCount = 6;
    public float spawnRadius = 12f;

    void Start(){
        InitializeScene();
    }

    void InitializeScene(){
        // 自动注入 FPS 操控组件if (Camera.main != null && Camera.main.GetComponent<CameraAutoMover>() == null)
            Camera.main.gameObject.AddComponent<CameraAutoMover>();

        CreateGameManager();
        CreateRedPacketTargets();
        Debug.Log(">>> 马年 AR 红包雷达初始化完毕!");
    }

    void CreateGameManager(){
        GameObject gmObj = new GameObject("AR_RedPacket_Manager");
        ARRedPacketManager manager = gmObj.AddComponent<ARRedPacketManager>();
        manager.totalRedPackets = redPacketCount;
    }

    void CreateRedPacketTargets(){
        GameObject prefab = CreateProceduralRedPacket();

        for (int i = 0; i < redPacketCount; i++)
        {
            // 在用户视线前方 120 度扇形区域随机分布float angle = Random.Range(-60f, 60f);
            float dist = Random.Range(3f, spawnRadius);
            Vector3 dir = Quaternion.Euler(0, angle, 0) * Vector3.forward;
            Vector3 spawnPos = Camera.main.transform.position + dir * dist + Vector3.up * Random.Range(-0.5f, 1.5f);

            GameObject rp = Instantiate(prefab, spawnPos, Quaternion.identity);
            rp.GetComponent<RedPacketTarget>().redPacketId = $"RP_2026_{i}";
        }
    }

    GameObject CreateProceduralRedPacket(){
        GameObject go = new GameObject("RedPacket_Prefab");
        SpriteRenderer sr = go.AddComponent<SpriteRenderer>();

        // 动态绘制像素纹理,规避外部资源依赖
        Texture2D tex = new Texture2D(64, 80);
        for (int y = 0; y < 80; y++) {
            for (int x = 0; x < 64; x++) {
                Color c = Color.red;
                // 画个金色的圆点代表马年金币if (Vector2.Distance(new Vector2(x, y), new Vector2(32, 45)) < 12) c = Color.yellow;
                tex.SetPixel(x, y, c);
            }
        }
        tex.Apply();

        sr.sprite = Sprite.Create(tex, new Rect(0, 0, 64, 80), new Vector2(0.5f, 0.5f));
        go.AddComponent<BoxCollider2D>().size = new Vector2(1, 1.2f);
        go.AddComponent<RedPacketTarget>();
        return go;
    }
}


第五章:效果演示——沉浸式寻宝全流程

为了直观展示这套逻辑在 Unity 模拟环境及未来真机上的表现,我们来看“视觉演示”:

当应用启动,用户处于初始位置。此时雷达扫描线开始高速旋转。在模拟环境中,我们按下 WASD 开始走动。此时雷达 UI 的 distanceLabel 还是静默的。随着用户旋转视角,当某个隐藏的红包进入前方 120 度扇形探测区,当用户最终走到红包面前(1.5 米内),系统不再需要任何按键确认,红包瞬间放大并得到消失(CaptureAnimation 协程)。

第六章:深度思考:如何真正适配 Rokid 灵珠 SDK?

作为一名开发者,我深知在 PC 环境下的流畅运行只是第一步。要让这款应用在 Rokid AR Lite 或 AR Studio 上散发光彩,我们需要做针对性的交互迁移。

1. 从键鼠平移到灵珠手柄(多模态交互)

在我的代码中,位置移动逻辑被封装在 HandleSpatialMove 中。在适配 Rokid 时,我会将 WASD 映射改为灵珠手柄的触摸板滑动(Touchpad)。 对于 Rokid AR Studio 用户,我更倾向于启用手势识别。当用户伸出手臂在空间中探索时,AR 红包的距离会被实时检测,手部的抓取动作(Pinch)将取代自动收集逻辑,让“抢红包”真正具备触觉上的仪式感。

2. 利用平面检测(Plane Detection)避免红包“穿模”

春节期间,家里的环境各异。利用 Rokid SDK 的平面检测能力,我可以优化 GenerateSpatialRedPackets 算法。不再是将红包生成在虚空中,而是将红包“放置”在眼镜识别出的地板、桌面甚至沙发靠背上。当红包被物理环境遮挡又由于透明度变化若隐若现时,AR 的虚实结合感将达到顶峰。

3. AI 语音的画龙点睛

2025 年是 AI 眼镜的转型年。在马年红包应用中,我们可以利用 Rokid 的语音识别接口。当雷达显示“探测到信号”时,用户大喊一声“马到成功!”,红包可以产生一个向用户飞来的吸引力效果。这种多模态交互(语音+AR)才是 AI 眼镜生态下的标准应用范式。

第七章:性能优化:让应用马不停蹄

AR 开发最怕的就是发热导致降频。为了保障春节聚会时的长时间使用,我采取了以下优化措施:

  • 距离剔除逻辑:虽然场景里有多个红包,但只有在 hintDistance 范围内的红包才会开启 Update 循环中的视觉动效。其余红包处于“逻辑在线但渲染静默”的状态。
  • 无资源轻量化:正如前面所说,程序化生成的贴图极大降低了显存带宽的压力。
  • UI 合批:所有的雷达指示器和状态文字均使用同一套自定义 UI Shader,确保 Draw Call 维持在个位数。

结语:AR 眼镜生态的未来,始于每一行代码

2026 马年到了。AI 眼镜已经不再是发烧友的玩物,它是通往空间计算世界的门票。通过这款“AR 红包雷达”应用,我想证明的是:哪怕是一个简单的春节游戏,只要深度结合了 FPS 级视角交互、空间距离感知和程序化动态生成,也能创造出前所未有的社交价值。

科技不应该是冷冰冰的代码。在春节团圆的时刻,让亲朋好友戴上 Rokid 眼镜,在笑声中一起寻找那些“跳跃在餐桌上”的福气,这正是我们开发者在这个 AI+AR 时代能给出的最暖心的献礼。

让我们一起,在 Rokid 开发者社区中,马不停蹄,解锁 AR 应用的无限可能!


参考资料:

CXR SDK 官方文档

UXR3.0 SDK 官方文档

Rokid 开发者论坛