基于 HarmonyOS 7.0 与 Flutter 的社交推理局跨端辅助工具:环形席位布局与流程控制实战
基于 HarmonyOS 7.0 与 Flutter 的社交推理局跨端辅助工具:环形席位布局与流程控制实战
前言
社交推理类桌游(Social Deduction Game)是一款经典的聚会互动游戏,起源于早期的校园逻辑游戏,此后传播至全球并衍生出无数变种规则。标准局通常由12名参与者组成,分为两大阵营:平民阵营(包括普通平民、侦查员、守护员等辅助角色)和对抗阵营(隐藏身份的对抗者)。游戏按“昼夜交替”的节奏推进——夜晚对抗方秘密选择目标、辅助人员依次发动技能;白天全体玩家讨论并投票决定淘汰对象。整个过程的魅力在于信息不完全博弈、逻辑识别和心理博弈的交织。本文将以 Flutter 跨端开发技术栈为基础,结合 HarmonyOS 7.0 的多设备协同能力,构建一套专业的社交推理局线下辅助应用,涵盖角色分配管理、环形席位可视化和阶段流程控制三大核心功能模块。
背景
社交推理局线下玩法与传统线上 APP 对局的最大区别在于需要一位“法官”(Host/主持人)来掌控游戏流程——宣布阶段切换、依次唤醒各角色执行技能、组织发言和投票、裁决淘汰结果。这位法官需要同时记住每位玩家的初始身份、存活状态、警长归属以及当前所处的游戏阶段,在12人局的信息量面前,这对人类短期记忆力是不小的挑战。现有的辅助工具大多面向在线文字/语音版设计,缺乏对线下聚会场景特殊需求的适配:线下场景需要的是“上帝视角”的信息集中展示(主持人能看到所有人身份,而玩家只能看到自己的)、物理围坐场景下的席位可视化(环形排列而非线性列表)、以及符合线下口令习惯的阶段按钮(“天黑请闭眼”、“对抗方请行动”、“侦查员请查验”)。因此,开发一款专门服务于线下主持人的移动端工具具有重要的实用价值,能够让游戏主持过程更加流畅专业,从而提升所有参与者的体验。
Flutter × HarmonyOS 7.0 跨端开发介绍
本项目的 UI 层完全基于 Flutter Widget 体系构建,核心亮点在于环形席位的网格近似布局和基于角色的动态颜色编码系统。Flutter 的 Wrap 组件配合 center 对齐方式实现了12个玩家卡片的环绕式排布效果,虽然并非真正的极坐标环形计算,但在移动端的方屏约束下,这是信息密度与视觉美感的最优平衡点。StatefulWidget 的状态管理机制追踪了轮次(_round)、当前阶段(_phase)和警长编号(_sheriff)三项核心游戏状态变量,每次 setState 触发都会重新计算存活人数和存活的对抗方数量,并刷新全部 UI。
在 HarmonyOS 7.0 平台上,该项目可以获得几项关键的系统能力增强:
- 分布式多屏协同:法官手机上运行主控界面的同时,可以将简化版的“当前阶段”显示流转到大屏幕电视或投影仪上供所有玩家实时观看,而不会暴露敏感身份信息。
- 振动反馈 API:可以为阶段切换提供触觉确认(如进入夜晚时一次短振模拟熄灯感、对抗方行动结束时两次短振)。
- 语音助手接口:可以支持法官用语音命令控制流程推进(如“天亮了”自动切换到投票阶段),释放双手专注于场务管理。
Flutter 的平台通道(Platform Channel)机制为这些原生能力的对接提供了标准化桥梁。

开发核心代码
核心一:游戏状态面板的四维统计与轮次追踪
int _round = 1;
String _phase = '天黑请闭眼';
int _sheriff = 3;
Widget _header(int alive, int opponents) {
return Container(
margin: const EdgeInsets.fromLTRB(20, 16, 20, 0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF16213E),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _wolfPrimary.withValues(alpha: 0.3)),
),
child: Column(children: [
Row(children: [
const Text('🎭 推理局',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w800)),
const Spacer(),
Text('第 $_round 轮',
style: TextStyle(color: _wolfPrimary, fontSize: 14, fontWeight: FontWeight.w800)),
]),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_wolfStat('存活', '$alive/12'),
_wolfStat('对抗方', '$opponents/4'),
_wolfStat('警长', '${_sheriff}号'),
_wolfStat('状态', _phase),
],
),
],
);
}
Widget _wolfStat(String label, String value) {
return Column(children: [
Text(value,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w900)),
Text(label,
style: const TextStyle(color: Color(0xFF6A6A8A), fontSize: 9)),
]);
}
游戏状态面板位于页面顶部,承担信息总览职能。外层容器使用比全局背景稍亮的深紫灰色 (#16213E) 作为背景,配合 30% 透明度的紫色边框建立视觉分区。面板内部垂直分为两个区域:首行 Row 左侧显示面具 emoji 和游戏名称(白色 18px 粗体),右侧显示当前轮次数(紫色 14px 粗体)——这种左标识右数据的布局模式在信息密度和可读性之间取得了良好平衡。
次行 Row 通过 spaceAround 均分四个统计项,每个由 _wolfStat 方法生成的 Column 构成双层结构:上层为 16px 白色超粗体数值(fontWeight.w900 确保在深色背景下足够醒目),下层为 9px 浅灰色标签名。四项指标的选取覆盖了主持人最频繁查询的信息维度——存活人数判断游戏是否接近终局、存活的对抗方数量评估当前局势、警长编号快速定位拥有额外投票权的玩家、当前阶段状态确认下一步该执行什么流程。这些数值并非硬编码,而是通过 build 方法开头的 where 过滤动态计算的(alive 统计 alive==true 的数量,opponents 统计 role=='对抗方' 且 alive==true 的数量),保证每次状态变更后数据的一致性。
核心二:环形席位的角色颜色编码与状态叠加
final _players = List.generate(12, (i) => {
'id': i + 1,
'alive': i != 4 && i != 8, // 5号和9号已淘汰
'role': i == 0 ? '侦查员' : i == 2 ? '守护员' : i == 3 ? '追猎员'
: i == 5 || i == 7 || i == 10 || i == 11 ? '对抗方' : '平民',
});
Widget _playerRing() {
return Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.center,
children: _players.map((p) {
final alive = p['alive'] as bool;
final role = p['role'] as String;
final id = p['id'] as int;
// 角色颜色映射:区分不同职能
final color = role == '对抗方' ? const Color(0xFFEF4444)
: role == '侦查员' ? const Color(0xFFF59E0B)
: role == '守护员' ? const Color(0xFF8B5CF6)
: role == '追猎员' ? const Color(0xFF10B981)
: const Color(0xFF6B7280);
final isSheriff = id == _sheriff;
return Container(
width: (MediaQuery.of(context).size.width - 60) / 4,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
// 存活时显示角色色半透明背景,淘汰时显示灰色
color: alive ? color.withValues(alpha: 0.1) : const Color(0xFF2A2A2A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: alive ? color.withValues(alpha: 0.3) : const Color(0xFF333333)),
),
child: Column(children: [
Stack(children: [
Container(/* 圆形头像容器 显示编号 */),
// 状态叠加:淘汰标记
if (!alive)
Positioned.fill(
child: Center(
child: Icon(Icons.close,
color: const Color(0xFFEF4444).withValues(alpha: 0.6), size: 28),
),
),
// 状态叠加:警长标记
if (isSheriff && alive)
Positioned(top: -4, right: -4,
child: Container(/* 金色警长星标 */),
),
]),
const SizedBox(height: 4),
Text(role, /* 角色名 根据存活调色 */),
]),
);
}).toList(),
),
);
}
玩家席位区域是整个页面的核心可视化组件,也是技术复杂度最高的部分。采用 Wrap 布局配合 center 对齐实现 12 个卡片的网格近似环绕效果(实际呈现为 4 列 3 行的整齐矩阵,但视觉重心集中在中央圆桌区域)。每张卡片的数据来源是 _players 列表中的一项,包含 id 编号、alive 布尔状态和 role 角色字符串三个关键字段。
角色的颜色映射是最关键的设计决策——五种角色各配专属色:对抗方为红色 (#EF4444) 象征紧张局势,侦查员为琥珀色 (#F59E0B) 代表洞察之光,守护员为紫色 (#8B5CF6) 呼应防护意象,追猎员为绿色 (#10B981) 关联行动力,平民为灰色 (#6B7280) 表达基础构成。这套配色不仅驱动了卡片的背景色 (10% 透明度) 和边框色 (30% 透明度),还决定了圆形头像区的填充色 (20% 透明度) 和编号文字色,形成了高度统一的角色视觉标识系统。
卡片内部的 Stack 叠层布局是实现多重状态叠加的关键技术手段。底层是 36×36 像素的圆形头像容器显示玩家编号(14px 超粗体角色色),其上通过 Positioned.fill 叠加了两层条件渲染的内容:
- 淘汰状态的标记:使用
Icons.close(28px, 60% 透明度红),仅当!alive时渲染,形成“被划掉”的视觉效果。 - 警长星标徽章:使用
if (isSheriff && alive)双重守卫,确保只有存活且持有警长身份的玩家才显示该标记,并通过top:-4 right:-4的负边距定位使其悬浮于圆形头像的右上角外侧。
Stack 的这种条件叠加机制使得单一组件能够同时表达“角色身份 + 存活状态 + 警长归属”三维信息,而不增加额外的空间占用。

核心三:阶段控制的卡牌式按钮与流程状态机
String _phase = '天黑请闭眼';
Widget _phaseControl() {
final phases = ['天黑', '对抗方', '侦查员', '守护员', '天亮', '投票'];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF16213E),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: phases.map((p) {
// 判断按钮是否激活
final active = _phase.contains(p) || (p == '天黑' && _phase == '天黑请闭眼');
return GestureDetector(
onTap: () => setState(() {
// 动态生成完整的阶段口令
_phase = p == '天黑' ? '天黑请闭眼'
: p == '投票' ? '请投票'
: '$p请行动';
}),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: active ? _wolfPrimary.withValues(alpha: 0.2) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Text(p,
style: TextStyle(
color: active ? _wolfPrimary : const Color(0xFF6A6A8A),
fontSize: 11, fontWeight: FontWeight.w800)),
),
);
}).toList(),
),
);
}
阶段控制栏位于页面底部,是主持人最频繁交互的区域。外层容器使用深紫灰背景 (#16213E) 和圆角矩形造型模拟一张横置的控制台面板。六个阶段按钮(天黑 → 对抗方 → 侦查员 → 守护员 → 天亮 → 投票)通过 Row 的 spaceAround 均匀分布,形成标准的流程序列。
每个按钮的激活状态 (active) 通过 _phase 字符串的部分匹配来判断。由于内部存储的 _phase 值为完整的口令文本(如“天黑请闭眼”、“对抗方请行动”),而按钮标签仅为关键词(如“天黑”、“对抗方”),所以使用 contains 方法进行子串匹配,并特别处理了“天黑”与“天黑请闭眼”的对应关系。激活态按钮背景填充 20% 透明度的紫色,产生高亮效果,文字变为纯紫色;非激活态则为完全透明背景搭配弱化灰色文字。
点击事件的 setState 回调根据按钮标签动态生成对应的完整口令文本,赋值给 _phase 变量触发全局重绘。这是一个简化的状态机实现,虽然不具备完整的合法性校验(如不允许跳过阶段),但对于线下辅助场景来说已经足够实用,主持人可以根据实际情况灵活调整流程顺序。
心得
开发这款社交推理局辅助应用的过程中,最大的技术收获来自于对“信息可见性”这一设计课题的深入思考。线下玩法与线上对局的根本区别在于信息不对称性——在 APP 中每位玩家只能看到自己的身份牌,而主持人掌握全部信息;而在面对面玩法中,主持人同样需要知道所有人的身份来正确引导流程,但又要避免在投影展示时不小心暴露给玩家。这意味着 UI 设计必须在同一套界面中同时支持“上帝视角”(主持人手持查看时显示全部角色信息)和“观众视角”(投屏显示时隐藏敏感信息)两种模式的切换,或者至少在架构上预留这种双模渲染的扩展点。当前版本专注于上帝视角的完整信息展示,将多模适配留作后续迭代方向,但这种“从一开始就考虑信息安全边界”的思维习惯,对于涉及隐私数据的任何应用开发都是至关重要的经验教训。
角色颜色编码系统的设计经历了几轮重要演进。最初方案是所有玩家卡片统一使用相同的蓝紫色主题色,仅通过文字区分角色,这在快速扫览时效率低下,主持人必须逐个阅读文字才能获取分布概况。第二版尝试为每个阵营使用双色方案(平民阵营蓝色/对抗阵营红色),但特殊职能角色的独特性被淹没了——侦查员的信息价值远高于普通平民,却无法在视觉上体现。最终确定的五色方案为每种角色分配独立的色彩标识,使得主持人一眼就能扫描出场上还有哪些特殊职能存活、对抗方分布在哪些号码位置、警长是否尚在。这种“可扫描性 (Scannability)”的设计目标对于需要频繁获取概览信息的场景尤为重要,类似于飞机驾驶舱仪表盘的设计哲学——飞行员不能逐个读数,而是要在瞬间把握整体态势。
玩家卡片的 Stack 叠加技术是实现多维状态表达的优雅方案。传统的做法可能是为每种状态组合准备不同的 Widget 分支(存活+非警长、存活+警长、淘汰+非警长、淘汰+警长…共四种组合),导致代码冗余且难以维护。而使用 Stack 的条件子节点方式,每种状态作为一个独立的视觉层,通过布尔守卫控制显隐,新增状态只需追加一个新的 Positioned 子节点,而不影响已有层的代码结构。这种“正交组合”的思想源自函数式编程中的 Option/Monad 概念,在 UI 开发中同样适用——每个视觉维度(角色色、淘汰标记、警长徽章)独立定义,然后通过组合产生所有可能的状态变体。
总结
本文系统地阐述了基于 Flutter 跨端框架开发的社交推理局辅助应用的完整技术实现方案。从四维统计数据的状态总览面板,到五色角色编码的环形席位网格,再到口令驱动的阶段流程控制器,每个模块都围绕“主持人视角”的信息需求进行了针对性设计。StatefulWidget 追踪轮次、阶段和警长三项核心状态变量,通过 where 过滤器动态计算存活人数和对抗方数量,确保数据一致性。Stack 条件叠加机制实现了角色色、淘汰标记和警长徽章的正交组合渲染。整个应用以深紫黑色为基调,配合紫罗兰色主强调色和多彩的角色标识系统,在营造神秘氛围的同时,保持了优秀的功能性信息传达效率。
从 HarmonyOS 7.0 跨端生态的角度展望,这款社交推理局辅助工具具有丰富的多设备协同扩展潜力。法官手机的完整控制界面可以与智慧屏/投影仪的观众视图形成主从联动——大屏仅显示当前阶段名称和存活人数,而不暴露任何角色身份信息。游戏过程中的重要事件(天黑、有人淘汰、游戏结束)可以通过 HarmonyOS 的分布式通知服务同步推送到所有参与者的手表或手机上,增强仪式感。语音控制接口的支持可以让主持人在双手忙于分发身份牌或操作道具时,用语音命令推进流程。更进一步,利用 HarmonyOS 的近场通信 (NFC) 能力,可以实现身份牌的数字化——每张实体 NFC 卡对应一个角色信息,法官用手机触碰即可自动登记分配,大幅提升开局准备效率。Flutter 框架在此过程中的核心价值在于提供了一套稳定高效且跨平台一致的 UI 抽象层,使开发者能够聚焦于逻辑博弈业务逻辑本身的完善,而无须为不同终端平台的渲染差异分散精力。
- 点赞
- 收藏
- 关注作者
评论(0)