Bevy 0.20 迁移指南:TextFont 默认字号改为 FontSize::Rem(1.),让 RemSize 真正成为根字号
本文基于 Bevy 仓库中的官方迁移文档 default_font_size_is_rem.md 展开,讲清 0.20 版本中 TextFont::default() 默认字号从 FontSize::Px(20.) 变为 FontSize::Rem(1.) 的动机与行为差异,并结合 bevy_text 与 bevy_ui 的源码说明 RemSize 资源如何参与字号求值与重排,最后给出"保持固定字号"与"跟随根字号缩放"两种迁移写法。读完后你能判断自己的文本代码在 0.20 下是否需要改动,以及改动点应该写在哪里。
一、变更内容:默认字号从固定 20px 变为 1rem
迁移文档给出的核心变更只有一句话:
TextFont::default()now usesFontSize::Rem(1.)instead ofFontSize::Px(20.), so that theRemSizeresource actually sets the default font size.
也就是说:
- 旧行为(0.19 及以前):没有显式设置字号的文本,字号被硬编码为 20 逻辑像素(
FontSize::Px(20.)),与RemSize资源无关; - 新行为(0.20):默认字号改为
FontSize::Rem(1.),即"1 个根字号单位",由RemSize资源决定。由于RemSize的默认值恰好是 20 逻辑像素,所以在默认配置下渲染结果完全一致,但从此以后,凡是保持默认字号的文本,都会随RemSize的修改而整体缩放。
文档给出的迁移建议是:如果你希望文本大小固定不变,就显式设置像素字号:
// 0.20
TextFont {
font_size: FontSize::Px(20.),
..default()
}
这个改动是 Bevy 文本子系统 rem/em 单位支持的一部分,与同 PR(#25231)中的另一篇迁移文档 val_em_and_rem.md 描述的 Val::Em / Val::Rem 变体、EmSize 组件、FontSize::eval 签名变更共同构成了 0.20 的"根字号"体系。
二、源码印证:TextFont::default() 现在返回 Rem(1.)
在 crates/bevy_text/src/text.rs 中,TextFont 的 Default 实现已经落实了这一变更:
impl Default for TextFont {
fn default() -> Self {
Self {
font: Default::default(),
font_size: FontSize::Rem(1.),
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
width: FontWidth::NORMAL,
font_features: FontFeatures::default(),
font_variations: FontVariations::default(),
font_smoothing: Default::default(),
}
}
}
需要注意一个容易混淆的细节:TextFont 的默认字号变了,但 FontSize 枚举自身的 Default 实现仍然是 Px(20.),见 crates/bevy_text/src/text.rs:
impl Default for FontSize {
fn default() -> Self {
Self::Px(20.)
}
}
因此,直接调用 TextFont::default()(绝大多数 UI/2D 文本路径都走这里)得到的是 Rem(1.);而单独使用 FontSize::default() 或 ..Default::default() 展开 font_size 字段的地方得到的仍是 Px(20.)。二者不一致正是这类迁移的常见"坑点",升级时建议全局搜索 FontSize::default() 和 TextFont::default() 确认行为是否符合预期。
三、RemSize:根字号资源的定义、注册与求值
3.1 资源定义与默认值
RemSize 定义在 crates/bevy_text/src/text.rs:
/// Default [`RemSize`] and [`EmSize`], in logical pixels
pub const DEFAULT_REM_SIZE_PX: f32 = 20.0;
/// The root font size, in logical pixels.
///
/// `Rem` units always resolve against this one global resource, including
/// `FontSize::Rem`, `LetterSpacing::Rem` and `Val::Rem`.
#[derive(Resource, Copy, Clone, Debug, PartialEq, Deref, DerefMut, Reflect)]
pub struct RemSize(pub f32);
impl Default for RemSize {
fn default() -> Self {
Self(DEFAULT_REM_SIZE_PX)
}
}
要点:
RemSize(pub f32)是单一全局Resource,单位为逻辑像素;默认值 20.0 对应常量DEFAULT_REM_SIZE_PX;- 文档注释明确了它的作用域:
FontSize::Rem、LetterSpacing::Rem和Val::Rem三种 rem 单位都只解析到这一个全局资源; - 该资源由
TextPlugin在插件构建时注册(init_resource::<RemSize>()),见 crates/bevy_text/src/lib.rs。也就是说,只要你的应用启用了文本功能,RemSize就可用,无需手动注册。
3.2 FontSize::eval:rem 如何变成像素
FontSize 是文本字号的枚举,支持 Px、Vw、Vh、VMin、VMax、Rem 六种单位,定义见 crates/bevy_text/src/text.rs。求值入口是 eval 方法(crates/bevy_text/src/text.rs):
impl FontSize {
/// Evaluate the font size to a value in logical pixels
pub fn eval(
self,
// Viewport size in logical pixels
logical_viewport_size: Vec2,
// Base Rem size in logical pixels
rem_size: RemSize,
) -> f32 {
match self {
FontSize::Px(s) => s,
FontSize::Vw(s) => logical_viewport_size.x * s / 100.,
FontSize::Vh(s) => logical_viewport_size.y * s / 100.,
FontSize::VMin(s) => logical_viewport_size.min_element() * s / 100.,
FontSize::VMax(s) => logical_viewport_size.max_element() * s / 100.,
FontSize::Rem(s) => rem_size.0 * s,
}
}
}
FontSize::Rem(s) 的求值就是 rem_size.0 * s:Rem(1.) 在当前默认配置下等于 20.0 * 1.0 = 20 逻辑像素,与旧版的 Px(20.) 数值相同——这就是迁移文档所说"renders identically"(渲染结果一致)的数学依据。
另一个值得注意的签名变化:0.20 中 eval 的第二个参数从裸 f32 改成了 RemSize 类型。这一点在 val_em_and_rem.md 中有专门记录:
// 0.19
let size = font_size.eval(logical_viewport_size, rem_size_px);
// 0.20
let size = font_size.eval(logical_viewport_size, RemSize(rem_size_px));
如果你自己封装了字号计算逻辑,这是需要跟着改的调用点。
3.3 渲染管线中的重排机制
默认字号变成 Rem 之后,"修改 RemSize 能全局缩放文本"这一行为是如何生效的?从源码结构看,答案在文本管线与重排标记里:
TextPipeline::update_buffer在遍历每个文本 span 时,如果发现字号是 rem 系单位,就把该文本块的uses_rem_sizes标记置为 true,见 crates/bevy_text/src/pipeline.rs:
match text_font.font_size {
crate::FontSize::Vw(_)
| crate::FontSize::Vh(_)
| crate::FontSize::VMin(_)
| crate::FontSize::VMax(_) => computed.uses_viewport_sizes = true,
crate::FontSize::Rem(_) => computed.uses_rem_sizes = true,
_ => (),
}
随后字号通过 text_font.font_size.eval(logical_viewport_size, base_rem_size) 求值(crates/bevy_text/src/pipeline.rs)。
ComputedTextBlock持有uses_rem_sizes标志(crates/bevy_text/src/text.rs),其needs_rerender方法会在RemSize发生变化且该文本块使用了 rem 单位时返回 true,从而触发重新排版(crates/bevy_text/src/text.rs):
pub fn needs_rerender(
&self,
is_viewport_size_changed: bool,
is_rem_size_changed: bool,
) -> bool {
self.needs_rerender
|| (is_viewport_size_changed && self.uses_viewport_sizes)
|| (is_rem_size_changed && self.uses_rem_sizes)
}
这个设计的实际效果是:只有使用了 rem(或视口)单位的文本才会因 RemSize 变化而重排,纯 Px 文本不受影响,避免不必要的全量重排开销。这也再次说明:0.20 之后"默认字号"的文本天然进入了 rem 依赖集合,而显式写成 Px 的文本则被排除在外——这正是迁移文档推荐"固定尺寸请显式写 Px"的底层原因。
四、对 UI 布局与文本输入的影响
RemSize 不只影响字号本身,它已经贯穿 UI 布局链路:
4.1 UI 节点的 font_size 转换
在 crates/bevy_ui/src/geometry.rs 中,Val 到 FontSize 的转换显示:未显式设置字号的 UI 节点(Val::Auto)会被转成 FontSize::Rem(1.),Val::Em(em) 也会被归一化为 FontSize::Rem(em)(em 相对于本节点字号解析,先折算成 rem 表达):
Val::Auto => FontSize::Rem(1.),
Val::Px(px) => FontSize::Px(px),
Val::Percent(percent) => FontSize::Rem(percent / 100.),
Val::Rem(rem) => FontSize::Rem(rem),
Val::Em(em) => FontSize::Rem(em), // see comment above
也就是说,没有手动设置字号的 UI 文本,在 0.20 中会统一走 rem 路径,这是本次迁移影响面最大的场景。
4.2 文本输入组件的重排条件
bevy_ui 的文本输入布局系统同样感知了这次变化。crates/bevy_ui/src/widget/text_input_layout.rs 中的重排条件明确加入了"字号是 rem 且 RemSize 已变化"的分支:
|| matches!(text_font.font_size, FontSize::Rem(_)) && rem_size.is_changed()
并在光标宽度等计算中直接用 text_font.font_size.eval(target.logical_size(), *rem_size) 求值(同文件 L515)。所以基于 TextInput 的组件在 RemSize 变化时也会正确跟随缩放。
4.3 同 PR 的其他连带变更
由于 rem 求值需要"根字号 + 节点字号"两个输入,同一 PR 还带来了 val_em_and_rem.md 记录的 API 变更,做 0.20 升级时应一并检查:
Val、Val2、UiPosition、CornerRadius、RadialGradientShape、UiTransform::compute_affine、BorderRadius的resolve/compute_affine方法新增em_size: EmSize与rem_size: RemSize两个参数;ComputedNode新增em_size、rem_size字段,用于记录布局时实际采用的字号,方便下游(如 box shadow)复用;Node现在要求EmSize组件(从bevy_text导出、经bevy_ui::prelude可用),节点有TextFont时会在TextFont、RemSize或渲染目标信息变化时自动重算EmSize;没有TextFont时该值由应用自行维护,默认值为DEFAULT_REM_SIZE_PX(20 逻辑像素),且不跟随RemSize的后续变化——需要跟随根字号的布局值应使用Val::Rem。
五、迁移实操:如何写代码
5.1 场景一:保持固定字号(推荐大多数游戏 UI)
如果你的文本尺寸是美术规格定的固定像素,按迁移文档显式写死即可:
use bevy::prelude::*;
fn setup(mut commands: Commands) {
commands.spawn(Text2d::new("Hello Bevy 0.20")).insert(TextFont {
font_size: FontSize::Px(20.),
..default()
});
}
也可以走链式构造器 TextFont::from_font_size / with_font_size(见 crates/bevy_text/src/text.rs),例如 TextFont::from_font_size(20_f32) 会构造出 Px 语义的字号。仓库示例中的既有写法本身就是这种风格,如 examples/2d/text2d.rs 里的 font_size: FontSize::Px(50.0)、examples/2d/texture_atlas.rs 中的 FontSize::Px(42.0),这些文本在 0.20 中行为不变。
5.2 场景二:让文本跟随根字号缩放
如果你希望做"全局字号调节"(例如设置面板里的 A/A+ 选项、高分辨率适配),现在只需修改 RemSize,所有保持默认(或显式 Rem)的文本会自动重排:
fn change_root_font_size(mut rem_size: ResMut<RemSize>) {
rem_size.0 = 32.0; // 所有 Rem(1.) 的默认文本变为 32 逻辑像素
}
生效链路是:RemSize 变化 → 使用 rem 字号的文本块 needs_rerender 为 true(crates/bevy_text/src/text.rs)→ 文本管线按 rem_size.0 * s 重新求值并排版(crates/bevy_text/src/pipeline.rs)。
注意一个行为差异:在旧版本中,同样的操作对"默认文本"完全无效(因为默认就是 Px(20.));升级后同一份代码里"没写字号"的文本会开始响应 RemSize。如果你在旧代码里依赖过"默认文本不会缩放"这一事实,需要在 0.20 中显式补上 FontSize::Px。
5.3 相关示例参考
- examples/2d/text2d.rs:2D 文本示例,演示显式
FontSize::Px的常规写法; - examples/ui/text/letter_spacing.rs:展示了
RemSize与LetterSpacing::Rem/LetterSpacing::Px之间的换算方式(LetterSpacing::Rem(v / rem_size.0)),是理解"一切 rem 单位都以RemSize为基准"的良好参考。
六、小结
| 项目 | 0.19 及以前 | 0.20 |
|---|---|---|
TextFont::default() 的字号 |
FontSize::Px(20.) |
FontSize::Rem(1.) |
FontSize::default() |
FontSize::Px(20.) |
仍为 FontSize::Px(20.) |
默认 RemSize |
—(对默认字号无作用) | 20.0 逻辑像素,且真正决定默认字号 |
修改 RemSize 的影响 |
不影响默认字号文本 | 所有默认/Rem 字号文本重排缩放 |
| 保持固定尺寸的写法 | 不写即可 | 显式 TextFont { font_size: FontSize::Px(20.), ..default() } |
本次变更本质上是把 Bevy 文本系统的"根字号"概念补齐:RemSize 从"仅供显式 Rem 单位使用的资源"升级为"所有默认文本字号的真正来源",并在渲染管线中通过 uses_rem_sizes 标记实现了按需重排。升级 0.20 时的检查清单很简单:确认默认字号文本是否需要固定(需要则显式写 Px);检查自定义代码中 FontSize::eval、Val::resolve 等 API 调用是否补上了 RemSize/EmSize 参数;以及留意 val_em_and_rem.md 中 Node 对 EmSize 的新要求。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00