Flipper Zero 上的 DnD Dice:TRPG 掷骰子应用的构建、使用与源码解析
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
DnD Dice是一款运行在 Flipper Zero 上的桌面掷骰子应用(FAP 外置应用),内置 Coin、d4、d6、d8、d10、d12、d20、d100 共 8 种骰子类型,支持一次投掷多枚骰子、自动求和并带有完整的滚动动画。本文基于 Applications/Official/source-OLDER/kyhwana/dice2/README.md 及配套源码,从编译构建、安装部署、实际操作到内部状态机与渲染原理逐层展开,读完你既能把它编译进自己的 Flipper Zero,也能完全理解这个应用背后的 GUI 编程范式。
DnD Dice 主界面截图
DnD Dice 投掷结果界面截图
功能概览:为 TRPG 桌面扮演而生的骰子盒
DnD Dice 面向《龙与地下城》等 TRPG 玩家,把现实中一整套骰子收纳进了 Flipper Zero 的 128×64 单色屏幕中。按原文档说明,应用支持以下骰子类型:
| 类型 | 面数 | 名称(源码中定义) | 典型用途 |
|---|---|---|---|
| Coin | 2 | Coin | 抛硬币判定、随机二选一 |
| d4 | 4 | d4 | 匕首、法术骰 |
| d6 | 6 | d6 | 属性骰、常见伤害骰 |
| d8 | 8 | d8 | 长弓、中型伤害骰 |
| d10 | 10 | d10 | 百分比骰的十位 |
| d12 | 12 | d12 | 巨斧等重武器 |
| d20 | 20 | d20 | 攻击检定、豁免、技能检定 |
| d100 | 100 | d100 | 百分比掷骰 |
在 constants.h 中可以看到这 8 种骰子被建模为一个Dice结构体数组,每种骰子只记录三个信息:type(面数)、屏幕坐标x/y和显示名称name。
除了骰子种类齐全,应用还有几个实用设计:
- 多枚投掷与自动求和:一次最多投掷 10 枚骰子(
MAX_DICE_COUNT常量),结果界面同时展示总和与每一枚的明细; - 动画反馈:投掷时骰子有逐帧滚动动画,结果数字会从屏幕上部落入一个结果边框内(
result_frame_pos_y定义了下落关键帧); - 合理的输入约束:Coin 与 d100 被设计为"单骰"类型(
isOneDice()),自动锁定数量为 1,避免逻辑上无意义的多次投掷。
仓库目录结构
在深入编译与源码之前,先整体认识一下这个应用的完整工程布局(位于Applications/Official/source-OLDER/kyhwana/dice2/):
dice2/ ├── README.md # 官方说明文档 ├── LICENSE.md # GPL-3.0 开源许可 ├── application.fam # FAP 应用清单(编译系统元数据) ├── constants.h # 常量、骰子表、状态机定义 ├── dice_app.c # 应用主逻辑(入口、事件循环、渲染) ├── icon.png # 应用列表图标(10×10) ├── assets/ # FAP 图标资源(骰子各帧、UI 按钮、计数与结果框) └── sources/ # Pixilart 像素画源文件与效果截图其中sources/目录保留了d20.pixil、d6.pixil、result_border.pixil等 Pixilart 像素画工程源文件(Pixil 2.6.1 格式,含图层与内嵌 base64 位图数据),说明应用的全部美术资源都是像素画风格,方便开发者直接用 Pixilart 编辑后重新导出。
编译构建:从源码到 .fap 文件
原文档给出了标准的 Flipper 外置应用编译流程。Flipper Zero 的应用以FAP(Flipper Application Package)形式分发,需要借助官方固件仓库的fbt(Flipper Build Tool)构建系统来交叉编译。完整步骤如下:
第一步:获取固件源码
克隆 flipperzero-firmware 官方仓库,或使用你日常刷写的第三方固件(如 unleashed-firmware)。编译产物会因固件分支的 API 版本差异而不同,因此选择与你的设备固件匹配的分支最重要。
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git第二步:把 dice2 工程链接进固件的用户应用目录
固件源码中有一个专用的用户应用目录applications_user,凡是要以 FAP 方式单独编译的应用,都需要在这里被引用。原文档要求创建名为dice的符号链接,指向本仓库(dice2 目录):
cd flipperzero-firmware ln -s /path/to/dice2 applications_user/dice说明:符号链接名
dice需要与工程内部实际使用的名称区分开——应用真正的appid是DND_Dice_app(见下文application.fam解析),符号链接名只是让fbt能在用户应用目录中发现该工程的路径。
第三步:编译
在固件仓库根目录执行:
./fbt fap_dice_dnd_app这里的fap_dice_dnd_app是 fbt 自动生成的 FAP 构建目标,命名规则为fap_<appid>,因此对应appid="DND_Dice_app"。编译系统会调用交叉工具链将 dice_app.c 与constants.h编译为可执行 FAP 包,并把assets/中的图标资源一并打包进去。
第四步:定位产物
编译完成后,FAP 文件位于固件构建目录:
build/f7-firmware-D/.extapps/dice_dnd_app.fapf7-firmware-D表示 debug 配置的 F7 芯片固件构建目录(release 配置为f7-firmware-R);.extapps是 fbt 收集所有外置应用产物的目录。
安装部署:把 FAP 装进 SD 卡
得到dice_dnd_app.fap后,原文档推荐两种安装方式:
- 手动拷贝:将
.fap文件复制到 Flipper Zero SD 卡的apps/Games目录(这与application.fam中fap_category="Games"的设置一致),然后从应用的Games(游戏)分类中启动; - qFlipper 桌面客户端:通过 qFlipper(Flipper 官方 PC 管理工具)的"文件管理/应用安装"功能直接推送,自动处理 SD 卡目录结构与依赖检查。
安装后设备端会显示DnD Dice [Ka3u6y6a](该名称来自application.fam的name字段),并配有icon.png作为列表图标。
玩法与按键操作
在设备上启动应用后,操作逻辑如下(对应 dice_app.c 中的按键处理分支):
| 按键 | 场景 | 行为 |
|---|---|---|
| ◀ / ▶ | 骰子选择界面 | 在 Coin、d4、d6、d8、d10、d12、d20、d100 之间切换,伴随骰子滑入的切换动画 |
| ▲ / ▼ | 骰子选择界面 | 增减投掷数量(1~10,Coin 与 d100 锁定为 1) |
| OK | 任意非动画状态 | 投掷骰子,进入滚动动画并展示结果 |
| BACK | 结果界面 | 返回骰子选择界面 |
| BACK | 骰子选择界面 | 退出应用 |
结果界面有三个信息层次:中央放大显示所有骰子点数总和(roll_result),下方以4, 1, 3形式列出逐枚明细(rolled_dices[]),底部则提示当前骰子类型与投掷数量。对于 Coin 和 d100 这类单骰类型,结果界面只显示总和而省略明细行(由isResultVisible()与isOneDice()共同控制)。
源码解析:状态机驱动的动画应用
抛开"掷骰子"的表象,这个应用是一份相当完整的Flipper Zero GUI 编程范例,其核心可以拆解为状态机、事件循环、掷骰算法和渲染管线四部分。
1. 六态状态机
应用在 constants.h 中定义了 6 个应用状态:
typedef enum { SelectState, // 骰子选择(主界面) SwipeLeftState, // 左滑切换动画(选中下一个骰子) SwipeRightState, // 右滑切换动画(选中上一个骰子) AnimState, // 投掷滚动动画 AnimResultState, // 结果数字下落动画 ResultState // 结果展示 } AppState;整个应用的交互可以看作这个状态机的转移图:SelectState在左右按键下进入滑动动画状态,滑动到位(坐标命中DICE_X)回到SelectState;OK 按键触发roll()进入AnimState,动画播完进入AnimResultState,数字落到位置后再转入ResultState;BACK 则从结果态回到选择态或直接退出。constants.h中isDiceNameVisible()、isDiceButtonsVisible()、isDiceSettingsDisabled()、isAnimState()等一系列谓词函数,都是围绕状态机来裁剪 UI 元素的显隐,例如动画过程中隐藏 ROLL 按钮、结果展示阶段隐藏左右切换箭头。
2. 事件驱动的消息循环
入口函数dice_dnd_app()(dice_app.c)遵循 Flipper SDK 的标准应用骨架:
- 分配容量为 8 的
FuriMessageQueue事件队列,消息类型为AppEvent(区分EventTypeTick定时器事件与EventTypeKey按键事件); - 注册
ViewPort的绘制回调draw_callback与输入回调input_callback,二者都通过ValueMutex互斥锁保护共享的State,避免渲染线程与输入线程竞争; - 创建一个周期为
furi_kernel_get_tick_frequency() * 0.2(约 200ms)的FuriTimer,定时器回调不断向队列投递EventTypeTick——动画的逐帧推进正是依赖这个 5Hz 的节拍; - 主循环
furi_message_queue_get()以 100ms 超时轮询队列,分别处理 Tick(驱动update())与按键事件; - 退出时依次释放定时器、消息队列、互斥锁并移除 ViewPort,资源清理完整。
3. 掷骰算法
roll()函数(dice_app.c)的随机逻辑只有一行核心代码:
state->rolled_dices[i] = (rand() % dice_types[state->dice_index].type) + 1;即对每个骰子用rand() % 面数 + 1生成 1~N 的均匀分布点数,累加到roll_result得到总和,超出投掷数量的槽位置 0。随后根据当前骰子类型切换硬币动画帧(coin_set_end),并把状态推进到AnimState。对于硬币(dice_index == 0),投掷结果只有 1 或 2 两种,因此coin_set_start()/coin_set_end()会根据结果把动画首尾帧替换为正面(heads)或反面(tails)的帧序列。
4. 分层渲染管线
draw_callback采用"先 UI、再内容"的分层绘制策略(dice_app.c):
draw_ui()负责绘制底部按钮(ROLL / EXIT / BACK)、左右切换箭头、数量调节图标与数字、骰子类型名称;- 在结果态调用
draw_results():用canvas_draw_str_aligned居中绘制总和与明细,并把结果框I_ui_result_border沿result_frame_pos_y[] = {-30, -20, -10, 0}逐帧下落; - 在其他状态调用
draw_dice():从dice_frames[]中按(dice_index-1) * MAX_DICE_FRAMES + anim_frame取当前动画帧绘制,并利用x > 128 || x < -35的越界裁剪跳过屏幕外的骰子。
动画帧的资源组织也值得注意:dice_frames[]按"每种骰子 4 帧"的顺序平铺存储(d4 4 帧 → d6 4 帧 → … → d100 4 帧),索引计算即上述公式;硬币则特殊处理为 9 帧(MAX_COIN_FRAMES),并在AnimState的第 3 帧切换首帧以表现翻转。
构建元数据解析:application.fam 里的门道
FAP 应用的构建信息全部声明在 application.fam 中,理解这些字段对改造成自有应用很有价值:
| 字段 | 值 | 含义 |
|---|---|---|
appid | DND_Dice_app | 全局唯一应用 ID,参与 fbt 目标名fap_dice_dnd_app的生成 |
name | DnD Dice [Ka3u6y6a] | 设备端显示的应用名 |
apptype | FlipperAppType.EXTERNAL | 外置应用,以独立 FAP 包分发(区别于内置固件应用) |
entry_point | dice_dnd_app | 入口函数符号,即dice_app.c中的int32_t dice_dnd_app(void* p) |
cdefines | ["APP_DICE"] | 编译期宏定义,可在源码中条件编译 |
requires | ["gui"] | 声明依赖 GUI 子系统,fbt 据此做依赖校验 |
stack_size | 1 * 1024 | 应用线程栈大小(1KB) |
order | 90 | 应用在菜单中的排序权重 |
fap_icon | icon.png | 应用列表图标 |
fap_category | Games | 安装分类目录,对应 SD 卡apps/Games |
fap_icon_assets | assets | 打包进 FAP 的图标资源目录 |
值得注意stack_size仅为 1KB,这对于一个纯渲染、无持久化状态的小型 GUI 应用已经足够——它与furi_kernel_get_tick_frequency() * 0.2的定时器共同体现了 Flipper 生态"资源极度精简"的编程取向。
自定义与扩展建议
由于全部美术源文件(.pixil)都保留在sources/目录,且常量集中在constants.h,你可以非常低成本地个性化这个应用:
- 增加骰子面数:在
dice_types[]中追加条目(如 d30),同步扩充dice_frames[]的帧序列并修改DICE_TYPES; - 修改动画节奏:调整
MAX_COIN_FRAMES/MAX_DICE_FRAMES、SWIPE_DIST(当前 11px)与定时器周期(0.2秒); - 替换美术资源:用 Pixilart 打开
sources/*.pixil重新绘制,导出 PNG 后替换assets/中的同名文件并重新执行./fbt fap_dice_dnd_app。
小结
DnD Dice 虽小,却完整覆盖了 Flipper Zero 外置应用开发的全部链路:application.fam声明元数据 → fbt 交叉编译 → 符号链接接入applications_user→.fap产物 → 拷贝至 SD 卡apps/Games启动。而它内部六状态状态机、200ms 定时器驱动的逐帧动画、ViewPort+ValueMutex的并发渲染模型,更是学习 Flipper GUI 编程不可多得的精简范例。无论你是想直接编译一个骰子应用装进设备,还是以此为模板开发自己的 FAP,本文结合 dice_app.c、constants.h 与 application.fam 的逐层拆解,都可以作为你的起点。
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考