跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Kotlin大前端java

基于 Rokid AR 眼镜的聚会游戏助手开发实战

基于 Rokid AR 眼镜的聚会游戏助手开发实战。项目利用 Kotlin 语言结合 CXR-M SDK,通过蓝牙连接实现眼镜与手机通信。核心功能包括题目私密显示、倒计时同步及 TTS 语音播报。解决了蓝牙权限动态申请、SDK 连接回调处理及设备查找等技术难点。架构上采用 SDK 封装层解耦业务逻辑,支持真心话大冒险等多种游戏模式,提升聚会互动体验。

PgDevote发布于 2026/3/23更新于 2026/9/955 浏览
基于 Rokid AR 眼镜的聚会游戏助手开发实战

背景与需求

传统聚会游戏组织存在信息泄露与节奏失控问题。组织者需频繁查看手机题目,易被旁观者看到,且难以掌控游戏氛围。AR 眼镜方案可实现题目私密显示,让组织者回归参与者角色。

Rokid CXR-M SDK 提供了「提词器场景」能力,可将文字内容推送到眼镜屏幕显示,配合 TTS(语音合成)实现提示播报。

项目架构

核心原则是保持简单,应用包含三个核心类:

com.rokid.game/
├── MainActivity.kt      # 主界面,处理交互逻辑
├── data/
│   └── GameData.kt      # 数据模型和预设题目
└── sdk/
    └── RokidGlassesManager.kt  # SDK 封装层

将 SDK 封装单独放一层,使业务代码与 SDK 实现解耦,便于后续升级或替换方案。

开发步骤

1. 配置项目依赖

在 settings.gradle.kts 中配置仓库:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://s01.oss.sonatype.org/content/repositories/releases/") }
        maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") }
    }
}

在 app/build.gradle.kts 中添加依赖:

dependencies {
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}

注意:CXR-M SDK 需要 Android API 28+,记得在 defaultConfig 中设置 minSdk = 28。

2. 配置蓝牙权限

眼镜通过蓝牙连接,需在 AndroidManifest.xml 中声明权限:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Android 12+ 需要动态申请 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 权限:

// MainActivity.kt
private fun checkPermissions() {
    val perms = mutableListOf<String>()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        perms.add(Manifest.permission.BLUETOOTH_SCAN)
        perms.add(Manifest.permission.BLUETOOTH_CONNECT)
    }
    val notGranted = perms.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
    if (notGranted.isNotEmpty()) {
        ActivityCompat.requestPermissions(this, notGranted.toTypedArray(), 100)
    }
}

3. 设计数据模型

支持三种经典聚会游戏:真心话大冒险、你比我猜、我是谁。

// GameData.kt
enum class GameType(val displayName: String) {
    TRUTH_OR_DARE("真心话大冒险"),
    CHARADES("你比我猜"),
    WHO_AM_I("我是谁"),
    COUNTDOWN("数数字")
}

data class GameQuestion(
    val id: Int,
    val gameType: GameType,
    val content: String,
    val answer: String? = null,
    val isTruth: Boolean = true
)

预设数据硬编码在单例中,随机选题避免重复:

object GameData {
    val questions: List<GameQuestion> = listOf(
        GameQuestion(1, GameType.TRUTH_OR_DARE, "你最近一次哭是什么时候?", null, true),
        GameQuestion(6, GameType.TRUTH_OR_DARE, "给通讯录第 5 个人打电话说新年快乐", null, false),
        // ... 更多题目
    )

    fun getRandom(type: GameType, used: Set<Int>): GameQuestion? {
        val available = getByType(type).filter { it.id !in used }
        return if (available.isNotEmpty()) available.random()
        else getByType(type).random()
    }
}

4. 封装 SDK 交互

创建 RokidGlassesManager 单例封装所有与眼镜的交互。

// RokidGlassesManager.kt
object RokidGlassesManager {
    private val cxrApi: CxrApi by lazy { CxrApi.getInstance() }
    private var connectionCallback: ConnectionCallback? = null

    interface ConnectionCallback {
        fun onConnecting()
        fun onConnected()
        fun onDisconnected()
        fun onFailed(errorMsg: String)
    }

    val isConnected: Boolean get() = cxrApi.isBluetoothConnected
}

连接流程需初始化蓝牙、获取连接信息、再建立连接。注意 connectBluetooth 回调中 onConnectionInfo 方法必须实现。

查找眼镜设备遍历已配对蓝牙设备:

fun findRokidGlasses(adapter: BluetoothAdapter): BluetoothDevice? {
    if (ActivityCompat.checkSelfPermission(adapter.javaClass, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
        return null
    return adapter.bondedDevices.find { it.name?.contains("Rokid", ignoreCase = true) == true }
}

发送内容到眼镜使用 sendStream 方法:

fun sendGameContent(text: String, callback: SendCallback? = null): Boolean {
    if (!isConnected) {
        callback?.onFailed("眼镜未连接")
        return false
    }
    cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null)
    val status = cxrApi.sendStream(
        ValueUtil.CxrStreamType.WORD_TIPS,
        text.toByteArray(Charsets.UTF_8),
        "game.txt",
        object : SendStatusCallback {
            override fun onSendSucceed() { callback?.onSuccess() }
            override fun onSendFailed(e: ValueUtil.CxrSendErrorCode?) { callback?.onFailed(e?.name ?: "发送失败") }
        }
    )
    return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}

TTS 语音播报功能:

fun sendTts(text: String): Boolean {
    if (!isConnected) return false
    return if (cxrApi.sendTtsContent(text) == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
        cxrApi.notifyTtsAudioFinished()
        true
    } else false
}

5. 主界面逻辑

MainActivity.kt 负责用户交互。倒计时功能使用 CountDownTimer,最后 10 秒同步更新眼镜显示:

private fun startCountdown() {
    countdownTimer?.cancel()
    binding.tvCountdown.text = "60"
    countdownTimer = object : CountDownTimer(60000, 1000) {
        override fun onTick(millis: Long) {
            binding.tvCountdown.text = "${millis / 1000}"
            if (millis / 1000 <= 10) {
                sendToGlasses("⏱ 倒计时:${millis / 1000}秒")
            }
        }
        override fun onFinish() {
            binding.tvCountdown.text = "0"
            RokidGlassesManager.sendTts("时间到!")
        }
    }.start()
}

发送到眼镜的内容格式需保证清晰易读:

private fun buildDisplayText(): String = buildString {
    val q = currentQuestion ?: return ""
    appendLine("🎮 ${currentType.displayName}")
    appendLine()
    if (currentType == GameType.TRUTH_OR_DARE) {
        appendLine("────── ${if (q.isTruth) "真心话" else "大冒险"} ──────")
    } else {
        appendLine("────── 题目 ──────")
    }
    appendLine()
    appendLine(q.content)
    appendLine()
    appendLine("👆 手机点击下一题")
}

功能清单

功能说明
四种游戏真心话大冒险/你比我猜/我是谁/数字
随机出题自动避免重复
60 秒倒计时你比我猜模式专用,最后 10 秒同步眼镜
眼镜同步题目实时推送到眼镜显示
TTS 语音倒计时结束播报'时间到'
蓝牙连接自动查找已配对的 Rokid 设备

常见问题解决

  1. 题目用完后空指针:在 getRandom 方法中,当无可用题目时重新从所有题目中随机选。
  2. 倒计时精度:CountDownTimer 在某些设备有精度问题,建议 Handler + Runnable 方式实现更精确计时。
  3. 屏幕常亮:在 onCreate 中添加 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)。

局限与展望

当前版本不足:题目数量有限且硬编码;不支持自定义添加题目;无积分排行榜;仅支持单机。

后续改进方向:云端题库、自定义题目、多人局域网同步、增加狼人杀等游戏。

参考文档

CXR-M SDK 官方文档

目录

  1. 背景与需求
  2. 项目架构
  3. 开发步骤
  4. 1. 配置项目依赖
  5. 2. 配置蓝牙权限
  6. 3. 设计数据模型
  7. 4. 封装 SDK 交互
  8. 5. 主界面逻辑
  9. 功能清单
  10. 常见问题解决
  11. 局限与展望
  12. 参考文档

更多推荐文章

查看全部
  • 小米智能家居 Miloco 分离式部署指南
  • WebODM 开源无人机地图制作完全指南
  • JavaScript 文档对象(Document)核心属性实战解析
  • C 语言冒泡排序算法原理与实现
  • 阿里 RynnBrain 具身智能模型:30B MoE 时空记忆架构与机器人开发实战
  • OD 机试真题:机器人活动区域
  • 多模态与编程能力兼得:Kimi 新模型 K2.5 实测
  • C++ 实现自定义 String 类:告别 C 字符串陷阱
  • VSCode 关闭 GitHub Copilot 功能方法
  • Vivado 基础使用指南:D 触发器设计与烧录
  • Web 虚拟卡销售店铺实现方案
  • AI 时代技术民主化:文科生为何成最大受益者?
  • Docker 入门前置:容器虚拟化基础之 Namespace 空间隔离
  • 计算机专业大学生三年成长与职业发展规划
  • 宇树 G1 机器人有线与无线连接开发指南
  • n8n AI 自动化流程编排平台部署与使用实战
  • 从零搭建 AI 系统权限控制系统
  • Blob 文件格式详解与前端应用
  • C++ 静态成员与非静态成员详解
  • 红黑树从概念到手撕实现:平衡树的工程取舍与核心逻辑

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online