Flipper Zero 上的 DnD Dice:TRPG 掷骰子应用的构建、使用与源码解析
2026/9/14 3:50:09 网站建设 项目流程

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 单色屏幕中。按原文档说明,应用支持以下骰子类型:

类型面数名称(源码中定义)典型用途
Coin2Coin抛硬币判定、随机二选一
d44d4匕首、法术骰
d66d6属性骰、常见伤害骰
d88d8长弓、中型伤害骰
d1010d10百分比骰的十位
d1212d12巨斧等重武器
d2020d20攻击检定、豁免、技能检定
d100100d100百分比掷骰

在 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.pixild6.pixilresult_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需要与工程内部实际使用的名称区分开——应用真正的appidDND_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.fap

f7-firmware-D表示 debug 配置的 F7 芯片固件构建目录(release 配置为f7-firmware-R);.extapps是 fbt 收集所有外置应用产物的目录。

安装部署:把 FAP 装进 SD 卡

得到dice_dnd_app.fap后,原文档推荐两种安装方式:

  1. 手动拷贝:将.fap文件复制到 Flipper Zero SD 卡的apps/Games目录(这与application.famfap_category="Games"的设置一致),然后从应用的Games(游戏)分类中启动;
  2. qFlipper 桌面客户端:通过 qFlipper(Flipper 官方 PC 管理工具)的"文件管理/应用安装"功能直接推送,自动处理 SD 卡目录结构与依赖检查。

安装后设备端会显示DnD Dice [Ka3u6y6a](该名称来自application.famname字段),并配有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.hisDiceNameVisible()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 中,理解这些字段对改造成自有应用很有价值:

字段含义
appidDND_Dice_app全局唯一应用 ID,参与 fbt 目标名fap_dice_dnd_app的生成
nameDnD Dice [Ka3u6y6a]设备端显示的应用名
apptypeFlipperAppType.EXTERNAL外置应用,以独立 FAP 包分发(区别于内置固件应用)
entry_pointdice_dnd_app入口函数符号,即dice_app.c中的int32_t dice_dnd_app(void* p)
cdefines["APP_DICE"]编译期宏定义,可在源码中条件编译
requires["gui"]声明依赖 GUI 子系统,fbt 据此做依赖校验
stack_size1 * 1024应用线程栈大小(1KB)
order90应用在菜单中的排序权重
fap_iconicon.png应用列表图标
fap_categoryGames安装分类目录,对应 SD 卡apps/Games
fap_icon_assetsassets打包进 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_FRAMESSWIPE_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询