从一个尴尬的春节聚会说起:我用 Rokid AR 眼镜做了个聚会游戏助手
从一个尴尬的春节聚会说起我用 Rokid AR 眼镜做了个聚会游戏助手今年春节我被委以重任——负责组织家里亲戚们的游戏环节。本以为简单的真心话大冒险却让我手忙脚乱一边在手机上翻找题目一边还要解释规则更要命的是每次我刚把题目看个大概旁边眼尖的表弟就已经喊出了答案。整个游戏下来我疲于奔命大家也玩得不尽兴。那一刻我就在想如果有一个设备能让我从容掌控游戏节奏同时又不暴露题目给所有人该多好直到我接触到 Rokid CXR-M SDK我意识到——这个想法可以实现。这篇文章就是我如何用这款 SDK 开发聚会游戏助手的完整记录。一、为什么是 AR 眼镜一个产品思考在动手写代码之前我花了不少时间思考为什么不用手机 App 就够了场景手机方案AR眼镜方案组织者状态眼睛盯着手机屏幕抬头看向参与者题目保密容易被旁人看到只有组织者可见游戏氛围“等等我看下题”流畅自然时间把控需要看时钟倒计时直接显示核心差异在于手机方案把组织者变成了管理员而眼镜方案让组织者回归参与者。Rokid 的 CXR-M SDK 提供了「提词器场景」——这正是我需要的将文字内容推送到眼镜屏幕显示。配合 TTS语音合成能力还能在游戏开始或结束时播放提示二、项目架构简单但不简陋这个项目的核心原则是保持简单——毕竟只是一个聚会小工具。整个应用只有三个核心类com.rokid.game/ ├── MainActivity.kt# 主界面处理所有交互逻辑├── data/ │ └── GameData.kt# 数据模型和预设题目└── sdk/ └── RokidGlassesManager.kt# SDK 封装层为什么把 SDK 封装单独放一层因为我想让业务代码与 SDK 实现解耦。如果将来 SDK 升级或者换成其他方案只需要修改这一个文件。三、Step by Step从零开始的开发过程第一步配置项目依赖首先是引入 CXR-M SDK。在 settings.gradle.kts 中配置仓库// settings.gradle.ktsdependencyResolutionManagement{repositories{google()mavenCentral()maven{urluri(https://s01.oss.sonatype.org/content/repositories/releases/)}maven{urluri(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第二步配置蓝牙权限眼镜通过蓝牙与手机连接需要在 AndroidManifest.xml 中声明权限:uses-permission android:nameandroid.permission.BLUETOOTH/uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN/uses-permission android:nameandroid.permission.BLUETOOTH_SCAN/uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT/uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/踩坑提示Android 12 需要动态申请 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 权限在代码中要处理这个逻辑// MainActivity.kt private funcheckPermissions(){val permsmutableListOfString()if(Build.VERSION.SDK_INTBuild.VERSION_CODES.S){perms.add(Manifest.permission.BLUETOOTH_SCAN)perms.add(Manifest.permission.BLUETOOTH_CONNECT)}val notGrantedperms.filter{ContextCompat.checkSelfPermission(this, it)!PackageManager.PERMISSION_GRANTED}if(notGranted.isNotEmpty()){ActivityCompat.requestPermissions(this, notGranted.toTypedArray(),100)}}第三步设计数据模型我选择了三种经典聚会游戏真心话大冒险、你比我猜、我是谁。数据模型的设计直接影响后续代码的复杂度所以我在这里花了不少心思// 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: Booleantrue// 真心话大冒险需要区分真心话/大冒险)这里有一个设计细节answer 字段是可空的因为真心话大冒险和你比我猜不需要答案显示。而 isTruth 字段只对真心话大冒险有意义用于在眼镜上显示「真心话」还是「大冒险」的标题。预设数据我直接硬编码在 GameData 单例中// GameData.kt object GameData{val questions: ListGameQuestionlistOf(// 真心话 GameQuestion(1, GameType.TRUTH_OR_DARE,你最近一次哭是什么时候, null,true), GameQuestion(2, GameType.TRUTH_OR_DARE,你最尴尬的经历是什么, null,true), GameQuestion(3, GameType.TRUTH_OR_DARE,你有暗恋的人吗, null,true), // 大冒险 GameQuestion(6, GameType.TRUTH_OR_DARE,给通讯录第5个人打电话说新年快乐, null,false), GameQuestion(7, GameType.TRUTH_OR_DARE,模仿一个动物叫声, null,false), // 你比我猜 GameQuestion(11, GameType.CHARADES,包饺子, null), GameQuestion(12, GameType.CHARADES,放鞭炮, null), // 我是谁 GameQuestion(17, GameType.WHO_AM_I,孙悟空,西游记角色), GameQuestion(18, GameType.WHO_AM_I,奥特曼,动漫角色), //... 更多题目)}随机选题要避免重复我实现了一个简单但有效的方法// GameData.kt fun getRandom(type: GameType, used: SetInt): GameQuestion?{val availablegetByType(type).filter{it.id!in used}// 如果全部用完了就从所有题目中随机选returnif(available.isNotEmpty())available.random()elsegetByType(type).random()}第四步封装 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)}interface SendCallback{fun onSuccess()fun onFailed(errorMsg: String)}val isConnected: Boolean get()cxrApi.isBluetoothConnected}连接眼镜的流程稍微复杂一些需要先初始化蓝牙、获取连接信息、再建立连接// RokidGlassesManager.kt fun connectGlasses(context: Context, device: BluetoothDevice){connectionCallback?.onConnecting()cxrApi.initBluetooth(context, device, object:BluetoothStatusCallback(){override fun onConnectionInfo(uuid: String?, mac: String?, account: String?, type: Int){if(!uuid.isNullOrEmpty()!mac.isNullOrEmpty()){cxrApi.connectBluetooth(context, uuid, mac, object:BluetoothStatusCallback(){override funonConnected(){connectionCallback?.onConnected()}override funonDisconnected(){connectionCallback?.onDisconnected()}override fun onFailed(e: ValueUtil.CxrBluetoothErrorCode?){connectionCallback?.onFailed(e?.name ?:连接失败)}// 需要空实现这个方法即使我们不使用它 override fun onConnectionInfo(a: String?, b: String?, c: String?, d: Int){}})}else{connectionCallback?.onFailed(获取连接信息失败)}}override funonConnected(){connectionCallback?.onConnected()}override funonDisconnected(){connectionCallback?.onDisconnected()}override fun onFailed(e: ValueUtil.CxrBluetoothErrorCode?){connectionCallback?.onFailed(e?.name ?:连接失败)}})}踩坑提示connectBluetooth 的回调中onConnectionInfo 方法必须实现否则可能无法正常回调 onConnected。这个问题困扰了我好几个小时。查找眼镜设备的逻辑很简单就是遍历已配对的蓝牙设备// RokidGlassesManager.kt fun findRokidGlasses(adapter: BluetoothAdapter): BluetoothDevice?{if(ActivityCompat.checkSelfPermission(adapter.javaClass, Manifest.permission.BLUETOOTH_CONNECT)!PackageManager.PERMISSION_GRANTED)returnnullreturnadapter.bondedDevices.find{it.name?.contains(Rokid, ignoreCasetrue)true}}发送内容到眼镜是核心功能。CXR-M SDK 的提词器场景通过 sendStream 方法发送文本// RokidGlassesManager.kt fun sendGameContent(text: String, callback: SendCallback?null): Boolean{if(!isConnected){callback?.onFailed(眼镜未连接)returnfalse}// 先激活提词器场景 cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null)// 再发送内容 val statuscxrApi.sendStream(ValueUtil.CxrStreamType.WORD_TIPS, text.toByteArray(Charsets.UTF_8),game.txt, object:SendStatusCallback(){override funonSendSucceed(){callback?.onSuccess()}override fun onSendFailed(e: ValueUtil.CxrSendErrorCode?){callback?.onFailed(e?.name ?:发送失败)}})returnstatusValueUtil.CxrStatus.REQUEST_SUCCEED}TTS 语音播报是锦上添花的功能可以在倒计时结束时播报时间到// RokidGlassesManager.kt fun sendTts(text: String): Boolean{if(!isConnected)returnfalsereturnif(cxrApi.sendTtsContent(text)ValueUtil.CxrStatus.REQUEST_SUCCEED){cxrApi.notifyTtsAudioFinished()true}elsefalse}第五步主界面逻辑主界面 MainActivity.kt 负责所有用户交互。我选择了简洁的设计顶部显示游戏类型和当前题目底部是操作按钮。游戏类型切换的逻辑// MainActivity.kt private fun selectGameType(type: GameType){currentTypetypeusedQuestions.clear()// 切换游戏时清空已用题目 binding.tvGameType.texttype.displayName nextQuestion()updateButtonStyles(type)}倒计时功能是你比我猜游戏的核心。我使用 Android 的 CountDownTimer并在最后 10 秒同步更新眼镜显示// MainActivity.kt private funstartCountdown(){countdownTimer?.cancel()binding.tvCountdown.text60countdownTimerobject:CountDownTimer(60000,1000){override fun onTick(millis: Long){binding.tvCountdown.text${millis/1000}// 最后10秒同步到眼镜if(millis /100010){sendToGlasses(⏱ 倒计时${millis/1000}秒)}}override funonFinish(){binding.tvCountdown.text0RokidGlassesManager.sendTts(时间到)}}.start()}发送到眼镜的内容格式需要精心设计保证在眼镜上显示清晰易读// MainActivity.kt private fun buildDisplayText(): StringbuildString{val qcurrentQuestion ?:returnappendLine(${currentType.displayName})appendLine()if(currentTypeGameType.TRUTH_OR_DARE){appendLine(────── ${if (q.isTruth) 真心话 else 大冒险} ──────)}else{appendLine(────── 题目 ──────)}appendLine()appendLine(q.content)appendLine()appendLine( 手机点击下一题)}四、实际使用体验开发完成后我在一次朋友聚会上测试了这个应用。使用流程是打开 APP选择游戏类型连接 Rokid 眼镜首次需要配对点击「发送到眼镜」题目出现在眼镜屏幕上游戏进行中用手机翻页或启动倒计时眼镜端的显示效果┌──────────────────────────────┐ │ 你比我猜 │ │ │ │ ────── 题目 ────── │ │ │ │ 包饺子 │ │ │ │ 手机点击下一题 │ └──────────────────────────────┘实际效果作为组织者我终于可以抬头面对参与者通过眼镜确认题目而不用低头看手机。游戏节奏明显更流畅了大家玩得也更尽兴。五、遇到的问题与解决问题一题目全部用完后怎么办最初的实现会导致空指针异常。解决方案是在 getRandom 方法中当没有可用题目时重新从所有题目中随机选fun getRandom(type: GameType, used: SetInt): GameQuestion?{val availablegetByType(type).filter{it.id!in used}returnif(available.isNotEmpty())available.random()elsegetByType(type).random()}问题二倒计时精度问题CountDownTimer 在某些设备上会有精度问题。对于聚会游戏这种场景秒级精度足够了但如果需要更精确的计时建议使用 Handler Runnable 的方式private val handlerHandler(Looper.getMainLooper())private var remainingSeconds60private val tickRunnableobject:Runnable{override funrun(){if(remainingSeconds0){remainingSeconds-- updateDisplay()handler.postDelayed(this,1000)}else{onTimeUp()}}}问题三屏幕常亮游戏过程中屏幕不能熄灭否则重新唤醒需要时间。解决方案是在 onCreate 中添加window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)六、功能清单功能说明四种游戏真心话大冒险/你比我猜/我是谁/数字随机出题自动避免重复60秒倒计时你比我猜模式专用最后10秒同步眼镜眼镜同步题目实时推送到眼镜显示TTS语音倒计时结束播报“时间到”蓝牙连接自动查找已配对的 Rokid 设备七、不足与展望当前版本的不足● 题目数量有限且硬编码在代码中● 不支持用户自定义添加题目● 没有积分和排行榜系统● 只支持单机模式后续可以改进的方向云端题库将题目存储在云端支持实时更新自定义题目允许用户添加自己的题目多人模式通过局域网实现多设备同步更多游戏增加狼人杀、谁是卧底等游戏八、结语这个项目虽然规模不大但让我深入理解了 AR 眼镜在日常生活中可能的应用场景。聚会游戏助手解决的不是一个技术难题而是一个体验问题——让组织者从管理员回归参与者。Rokid CXR-M SDK 的封装做得不错让开发者可以专注于业务逻辑而不用关心底层通信细节。提词器场景的设计也很巧妙非常适合这类需要私密显示内容的应用。如果你也有类似的想法不妨动手试试。代码量不大但成就感满满。项目源码PartyGameHelper/相关资源:CXR-M SDK 官方文档Rokid 开发者论坛

相关新闻

React Hook Form + Zod:优雅构建 React 表单

React Hook Form + Zod:优雅构建 React 表单

最近在浏览 shadcn/ui 的 表单文档 (Forms)。 时,发现它并没有去造一个全新的表单轮子,而是推荐使用两个神仙库:React Hook Form 和 Zod。 为什么 shadcn/ui 会选择这个组合?我使用下来总结有以下几点: react-hook-f…

2026/5/17 10:23:14 阅读更多 →
基于多目标算法的冷热电联供型综合能源系统运行优化 粒子群算法 平台:MATLAB

基于多目标算法的冷热电联供型综合能源系统运行优化 粒子群算法 平台:MATLAB

基于多目标算法的冷热电联供型综合能源系统运行优化 粒子群算法 平台:MATLAB冷热电联供系统玩的就是能源利用效率的最大化。这种系统同时产出冷、热、电能,但怎么分配负荷、调度设备可是个技术活。最近用MATLAB折腾了个粒子群优化方案,发现多…

2026/5/17 10:23:12 阅读更多 →
这次终于选对!全网爆红的AI论文写作软件 —— 千笔AI

这次终于选对!全网爆红的AI论文写作软件 —— 千笔AI

你是否在论文写作中感到力不从心?选题无从下手,框架混乱,文献查找费时费力,查重率高得让人焦虑,格式修改总也对不上?这些困扰让无数学生陷入“论文焦虑”。而如今,一款被全网爆红的AI论文写作软…

2026/7/5 11:01:42 阅读更多 →

最新新闻

实战指南:用FoundationPose实现6D物体姿态估计与跟踪的最佳实践

实战指南:用FoundationPose实现6D物体姿态估计与跟踪的最佳实践

实战指南:用FoundationPose实现6D物体姿态估计与跟踪的最佳实践 【免费下载链接】FoundationPose [CVPR 2024 Highlight] FoundationPose: Unified 6D Pose Estimation and Tracking of Novel Objects 项目地址: https://gitcode.com/gh_mirrors/fo/FoundationPos…

2026/7/5 16:00:53 阅读更多 →
锂电硬件级过压保护方案设计与STM32实现

锂电硬件级过压保护方案设计与STM32实现

1. 项目背景与核心器件选型锂离子电池因其高能量密度和长循环寿命,已成为便携式电子设备和储能系统的首选电源方案。但过充电是导致锂离子电池热失控甚至起火爆炸的主要诱因之一,这让我在去年开发户外储能电源时深有体会。当时测试组反馈,在快…

2026/7/5 15:58:53 阅读更多 →
Gemma-4 E4B技术深度解析:如何用4.5B有效参数实现多模态智能

Gemma-4 E4B技术深度解析:如何用4.5B有效参数实现多模态智能

Gemma-4 E4B技术深度解析:如何用4.5B有效参数实现多模态智能 【免费下载链接】gemma-4-E4B 项目地址: https://ai.gitcode.com/hf_mirrors/google/gemma-4-E4B 当你面对一个需要同时处理文本、图像、音频和视频的AI项目时,是否曾为选择合适模型而…

2026/7/5 15:56:41 阅读更多 →
Vue3企业级数据可视化大屏架构设计:应对多分辨率适配与实时渲染挑战

Vue3企业级数据可视化大屏架构设计:应对多分辨率适配与实时渲染挑战

Vue3企业级数据可视化大屏架构设计:应对多分辨率适配与实时渲染挑战 【免费下载链接】IofTV-Screen-Vue3 一个基于 vue3、vite、Echart 框架的大数据可视化(大屏展示)模板 项目地址: https://gitcode.com/gh_mirrors/io/IofTV-Screen-Vue3 …

2026/7/5 15:56:41 阅读更多 →
Gin-Vue-Admin代码生成器字段编辑:5个深度优化技巧与架构解析

Gin-Vue-Admin代码生成器字段编辑:5个深度优化技巧与架构解析

Gin-Vue-Admin代码生成器字段编辑:5个深度优化技巧与架构解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin的开发基础平台,支持TS和JS混用。它集成了JWT鉴权、权限管理、动态路由、显隐可控组件、分页封装、多点登录拦截、资源权限、上传下…

2026/7/5 15:54:41 阅读更多 →
3分钟掌握 facetype.js:终极字体转换工具完全指南

3分钟掌握 facetype.js:终极字体转换工具完全指南

3分钟掌握 facetype.js:终极字体转换工具完全指南 【免费下载链接】facetype.js typeface.js generator 项目地址: https://gitcode.com/gh_mirrors/fa/facetype.js facetype.js 是一个强大的在线字体转换工具,专门用于将标准字体文件转换为 type…

2026/7/5 15:54:41 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻