北京科技网站建设公司wordpress微信文章采集
北京科技网站建设公司,wordpress微信文章采集,国内外电子政务网站建设差距,网站开发报酬从一个尴尬的春节聚会说起#xff1a;我用 Rokid AR 眼镜做了个聚会游戏助手
今年春节#xff0c;我被委以重任——负责组织家里亲戚们的游戏环节。本以为简单的真心话大冒险#xff0c;却让我手忙脚乱#xff1a;一边在手机上翻找题目#xff0c;一边还要解释规则#…从一个尴尬的春节聚会说起我用 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 开发者论坛